package service_test import ( "context" "database/sql" "encoding/json" "os" "path/filepath" "testing" "bugulma/backend/internal/domain" "bugulma/backend/internal/repository" "bugulma/backend/internal/service" "bugulma/backend/internal/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/gorm" ) type GeographicalDataMigrationServiceTestSuite struct { suite.Suite db *gorm.DB siteRepo domain.SiteRepository geoRepo domain.GeographicalFeatureRepository sqliteDB *sql.DB tempDir string migrationSvc *service.GeographicalDataMigrationService } func (suite *GeographicalDataMigrationServiceTestSuite) SetupTest() { suite.db = testutils.SetupTestDBWithTestcontainers(suite.T()) suite.siteRepo = repository.NewSiteRepository(suite.db) suite.geoRepo = repository.NewGeographicalFeatureRepository(suite.db) // Create temporary directory and SQLite database suite.tempDir = suite.T().TempDir() sqlitePath := filepath.Join(suite.tempDir, "test_data.db") // Create test SQLite database with sample data err := suite.createTestSQLiteDB(sqlitePath) suite.Require().NoError(err) // Create migration service suite.migrationSvc, err = service.NewGeographicalDataMigrationService( suite.db, suite.geoRepo, suite.siteRepo, sqlitePath, ) suite.Require().NoError(err) // Create test organization org := &domain.Organization{ID: "org-migration-test", Name: "Migration Test Organization"} err = repository.NewOrganizationRepository(suite.db).Create(context.Background(), org) suite.Require().NoError(err) } func (suite *GeographicalDataMigrationServiceTestSuite) TearDownTest() { if suite.migrationSvc != nil { suite.migrationSvc.Close() } os.RemoveAll(suite.tempDir) } func TestGeographicalDataMigrationService(t *testing.T) { suite.Run(t, new(GeographicalDataMigrationServiceTestSuite)) } func (suite *GeographicalDataMigrationServiceTestSuite) createTestSQLiteDB(dbPath string) error { db, err := sql.Open("sqlite3", dbPath) if err != nil { return err } defer db.Close() // Create test tables createTablesSQL := ` CREATE TABLE osm_features ( id TEXT PRIMARY KEY, osm_type TEXT, osm_id TEXT, feature_type TEXT, geometry TEXT, properties TEXT ); CREATE TABLE osm_buildings ( id TEXT PRIMARY KEY, osm_type TEXT, osm_id TEXT, building_type TEXT, geometry TEXT, properties TEXT ); CREATE TABLE osm_roads ( id TEXT PRIMARY KEY, osm_type TEXT, osm_id TEXT, road_type TEXT, geometry TEXT, properties TEXT ); CREATE TABLE osm_green_spaces ( id TEXT PRIMARY KEY, osm_type TEXT, osm_id TEXT, green_space_type TEXT, geometry TEXT, properties TEXT ); ` _, err = db.Exec(createTablesSQL) if err != nil { return err } // Insert test data testData := []struct { table string data []map[string]interface{} }{ { table: "osm_features", data: []map[string]interface{}{ { "id": "building-1", "osm_type": "way", "osm_id": "1001", "feature_type": "building", "geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.41, 52.5], [13.41, 52.51], [13.4, 52.51], [13.4, 52.5]]]}`, "properties": `{"building": "office", "name": "Test Office Building"}`, }, { "id": "road-1", "osm_type": "way", "osm_id": "2001", "feature_type": "road", "geometry": `{"type": "LineString", "coordinates": [[13.4, 52.5], [13.42, 52.5]]}`, "properties": `{"highway": "primary", "name": "Test Road"}`, }, { "id": "park-1", "osm_type": "way", "osm_id": "3001", "feature_type": "green_space", "geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.405, 52.5], [13.405, 52.505], [13.4, 52.505], [13.4, 52.5]]]}`, "properties": `{"leisure": "park", "name": "Test Park"}`, }, }, }, { table: "osm_buildings", data: []map[string]interface{}{ { "id": "building-1", "osm_type": "way", "osm_id": "1001", "building_type": "office", "geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.41, 52.5], [13.41, 52.51], [13.4, 52.51], [13.4, 52.5]]]}`, "properties": `{"building": "office", "name": "Test Office Building", "levels": 5}`, }, }, }, { table: "osm_roads", data: []map[string]interface{}{ { "id": "road-1", "osm_type": "way", "osm_id": "2001", "road_type": "primary", "geometry": `{"type": "LineString", "coordinates": [[13.4, 52.5], [13.42, 52.5]]}`, "properties": `{"highway": "primary", "name": "Test Road", "surface": "asphalt"}`, }, }, }, { table: "osm_green_spaces", data: []map[string]interface{}{ { "id": "park-1", "osm_type": "way", "osm_id": "3001", "green_space_type": "park", "geometry": `{"type": "Polygon", "coordinates": [[[13.4, 52.5], [13.405, 52.5], [13.405, 52.505], [13.4, 52.505], [13.4, 52.5]]]}`, "properties": `{"leisure": "park", "name": "Test Park", "area": "small"}`, }, }, }, } for _, tableData := range testData { for _, record := range tableData.data { columns := []string{"id", "osm_type", "osm_id", "feature_type", "geometry", "properties"} if tableData.table != "osm_features" { switch tableData.table { case "osm_buildings": columns = []string{"id", "osm_type", "osm_id", "building_type", "geometry", "properties"} case "osm_roads": columns = []string{"id", "osm_type", "osm_id", "road_type", "geometry", "properties"} case "osm_green_spaces": columns = []string{"id", "osm_type", "osm_id", "green_space_type", "geometry", "properties"} } } values := make([]interface{}, len(columns)) for i, col := range columns { values[i] = record[col] } placeholders := "?, ?, ?, ?, ?, ?" query := "INSERT INTO " + tableData.table + " (" + columns[0] for i := 1; i < len(columns); i++ { query += ", " + columns[i] } query += ") VALUES (" + placeholders[:len(placeholders)-len(", ?")] + placeholders[len(placeholders)-3:] + ")" _, err := db.Exec(query, values...) if err != nil { return err } } } return nil } func (suite *GeographicalDataMigrationServiceTestSuite) TestNewGeographicalDataMigrationService() { assert.NotNil(suite.T(), suite.migrationSvc) } func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrateBuildingPolygons() { progress, err := suite.migrationSvc.MigrateBuildingPolygons(context.Background()) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), progress) assert.Equal(suite.T(), "Migrating Building Polygons", progress.CurrentOperation) assert.Greater(suite.T(), progress.TotalRecords, 0) // Verify buildings were migrated buildings, err := suite.geoRepo.GetByType(context.Background(), domain.GeographicalFeatureTypeLandUse) assert.NoError(suite.T(), err) assert.Len(suite.T(), buildings, progress.Successful) // Verify building data if len(buildings) > 0 { building := buildings[0] assert.Equal(suite.T(), domain.GeographicalFeatureTypeLandUse, building.FeatureType) assert.Equal(suite.T(), "way", building.OSMType) assert.NotEmpty(suite.T(), building.Properties) var props map[string]interface{} err := json.Unmarshal(building.Properties, &props) assert.NoError(suite.T(), err) assert.Contains(suite.T(), props, "building") assert.Contains(suite.T(), props, "name") } } func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrateRoadNetwork() { progress, err := suite.migrationSvc.MigrateRoadNetwork(context.Background()) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), progress) assert.Equal(suite.T(), "Migrating Road Network", progress.CurrentOperation) assert.Greater(suite.T(), progress.TotalRecords, 0) // Verify roads were migrated roads, err := suite.geoRepo.GetByType(context.Background(), domain.GeographicalFeatureTypeRoad) assert.NoError(suite.T(), err) assert.Len(suite.T(), roads, progress.Successful) // Verify road data if len(roads) > 0 { road := roads[0] assert.Equal(suite.T(), domain.GeographicalFeatureTypeRoad, road.FeatureType) assert.Equal(suite.T(), "way", road.OSMType) var props map[string]interface{} err := json.Unmarshal(road.Properties, &props) assert.NoError(suite.T(), err) assert.Contains(suite.T(), props, "highway") } } func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrateGreenSpaces() { progress, err := suite.migrationSvc.MigrateGreenSpaces(context.Background()) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), progress) assert.Equal(suite.T(), "Migrating Green Spaces", progress.CurrentOperation) assert.Greater(suite.T(), progress.TotalRecords, 0) // Verify green spaces were migrated greenSpaces, err := suite.geoRepo.GetByType(context.Background(), domain.GeographicalFeatureTypeGreenSpace) assert.NoError(suite.T(), err) assert.Len(suite.T(), greenSpaces, progress.Successful) // Verify green space data if len(greenSpaces) > 0 { greenSpace := greenSpaces[0] assert.Equal(suite.T(), domain.GeographicalFeatureTypeGreenSpace, greenSpace.FeatureType) assert.Equal(suite.T(), "way", greenSpace.OSMType) var props map[string]interface{} err := json.Unmarshal(greenSpace.Properties, &props) assert.NoError(suite.T(), err) assert.Contains(suite.T(), props, "leisure") assert.Equal(suite.T(), "park", props["leisure"]) } } func (suite *GeographicalDataMigrationServiceTestSuite) TestGenerateMigrationStatistics() { // Run migrations first suite.migrationSvc.MigrateBuildingPolygons(context.Background()) suite.migrationSvc.MigrateRoadNetwork(context.Background()) suite.migrationSvc.MigrateGreenSpaces(context.Background()) stats, err := suite.migrationSvc.GetMigrationStatistics(context.Background()) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), stats) // Verify expected statistics assert.Contains(suite.T(), stats, "sites") assert.Contains(suite.T(), stats, "green_space_total_area_km2") assert.Contains(suite.T(), stats, "road_network") // Check road network statistics structure roadStats, ok := stats["road_network"].(map[string]interface{}) assert.True(suite.T(), ok) assert.Contains(suite.T(), roadStats, "total_roads") assert.Contains(suite.T(), roadStats, "total_length_km") assert.Contains(suite.T(), roadStats, "avg_length_km") assert.Contains(suite.T(), roadStats, "max_length_km") } func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrationProgress_Tracking() { progress, err := suite.migrationSvc.MigrateBuildingPolygons(context.Background()) assert.NoError(suite.T(), err) // Verify progress tracking assert.GreaterOrEqual(suite.T(), progress.ProcessedRecords, 0) assert.GreaterOrEqual(suite.T(), progress.Successful, 0) assert.GreaterOrEqual(suite.T(), progress.Failed, 0) assert.Equal(suite.T(), progress.Successful+progress.Failed, progress.ProcessedRecords) assert.GreaterOrEqual(suite.T(), progress.ProgressPercent, 0.0) assert.LessOrEqual(suite.T(), progress.ProgressPercent, 100.0) } func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrationWithSiteMatching() { // Create a site that matches a building ID site := &domain.Site{ ID: "building-1", // Same ID as test building Name: "Matching Site", Latitude: 52.505, Longitude: 13.405, SiteType: domain.SiteTypeIndustrial, OwnerOrganizationID: "org-migration-test", } err := suite.siteRepo.Create(context.Background(), site) suite.Require().NoError(err) // Run building migration progress, err := suite.migrationSvc.MigrateBuildingPolygons(context.Background()) assert.NoError(suite.T(), err) assert.True(suite.T(), progress.Successful > 0) // Verify the site still exists (geometry is stored at database level, not in struct) updatedSite, err := suite.siteRepo.GetByID(context.Background(), "building-1") assert.NoError(suite.T(), err) assert.NotNil(suite.T(), updatedSite) } func (suite *GeographicalDataMigrationServiceTestSuite) TestClose() { err := suite.migrationSvc.Close() assert.NoError(suite.T(), err) // Should be able to close multiple times without error err = suite.migrationSvc.Close() assert.NoError(suite.T(), err) } func (suite *GeographicalDataMigrationServiceTestSuite) TestMigrationErrorHandling() { // Test with invalid SQLite path (service should handle this gracefully) invalidSvc, err := service.NewGeographicalDataMigrationService( suite.db, suite.geoRepo, suite.siteRepo, "/nonexistent/path.db", ) assert.Error(suite.T(), err) assert.Nil(suite.T(), invalidSvc) } func (suite *GeographicalDataMigrationServiceTestSuite) TestEmptyMigration() { // Create a service with empty SQLite database emptyDBPath := filepath.Join(suite.tempDir, "empty.db") emptyDB, err := sql.Open("sqlite3", emptyDBPath) suite.Require().NoError(err) // Create empty table _, err = emptyDB.Exec(`CREATE TABLE osm_features (id TEXT, osm_type TEXT, osm_id TEXT, feature_type TEXT, geometry TEXT, properties TEXT)`) suite.Require().NoError(err) emptyDB.Close() emptySvc, err := service.NewGeographicalDataMigrationService( suite.db, suite.geoRepo, suite.siteRepo, emptyDBPath, ) suite.Require().NoError(err) defer emptySvc.Close() // Run migration on empty data progress, err := emptySvc.MigrateBuildingPolygons(context.Background()) assert.NoError(suite.T(), err) assert.Equal(suite.T(), 0, progress.TotalRecords) assert.Equal(suite.T(), 0, progress.Successful) assert.Equal(suite.T(), 0, progress.Failed) assert.Equal(suite.T(), 100.0, progress.ProgressPercent) }