package service_test import ( "context" "testing" "bugulma/backend/internal/domain" "bugulma/backend/internal/geospatial" "bugulma/backend/internal/repository" "bugulma/backend/internal/service" "bugulma/backend/internal/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/datatypes" "gorm.io/gorm" ) type EnvironmentalImpactServiceTestSuite struct { suite.Suite db *gorm.DB siteRepo domain.SiteRepository geoRepo domain.GeographicalFeatureRepository geoCalc geospatial.Calculator geospatialSvc *service.GeospatialService envSvc *service.EnvironmentalImpactService } func (suite *EnvironmentalImpactServiceTestSuite) SetupTest() { suite.db = testutils.SetupTestDBWithTestcontainers(suite.T()) suite.siteRepo = repository.NewSiteRepository(suite.db) suite.geoRepo = repository.NewGeographicalFeatureRepository(suite.db) suite.geospatialSvc = service.NewGeospatialService(suite.db, suite.geoRepo) suite.geoCalc = geospatial.NewCalculatorWithDefaults() suite.envSvc = service.NewEnvironmentalImpactService(suite.geoRepo, suite.siteRepo, suite.geospatialSvc, suite.geoCalc) // Create test organization org := &domain.Organization{ID: "org-env-test", Name: "Environmental Test Organization"} err := repository.NewOrganizationRepository(suite.db).Create(context.Background(), org) suite.Require().NoError(err) } func TestEnvironmentalImpactService(t *testing.T) { suite.Run(t, new(EnvironmentalImpactServiceTestSuite)) } func (suite *EnvironmentalImpactServiceTestSuite) TestNewEnvironmentalImpactService() { assert.NotNil(suite.T(), suite.envSvc) } func (suite *EnvironmentalImpactServiceTestSuite) setupTestSites() []*domain.Site { sites := []*domain.Site{ { ID: "site-high-impact", Name: "High Impact Industrial Site", Latitude: 52.5200, Longitude: 13.4050, SiteType: domain.SiteTypeIndustrial, OwnerOrganizationID: "org-env-test", EnvironmentalImpact: "high_impact", }, { ID: "site-low-impact", Name: "Low Impact Office Site", Latitude: 52.5300, Longitude: 13.4150, SiteType: domain.SiteTypeOffice, OwnerOrganizationID: "org-env-test", EnvironmentalImpact: "low_impact", }, { ID: "site-eco-friendly", Name: "Eco-Friendly Site", Latitude: 52.5400, Longitude: 13.4250, SiteType: domain.SiteTypeRetail, OwnerOrganizationID: "org-env-test", EnvironmentalImpact: "eco_friendly", }, } for _, site := range sites { err := suite.siteRepo.Create(context.Background(), site) suite.Require().NoError(err) } return sites } func (suite *EnvironmentalImpactServiceTestSuite) setupTestGreenSpaces() { // Create some mock green space features // Note: In a real scenario, these would have actual geometry data greenSpaces := []*domain.GeographicalFeature{ { ID: "park-central", Name: "Central Park", FeatureType: domain.GeographicalFeatureTypeGreenSpace, OSMType: "way", OSMID: "1001", Properties: datatypes.JSON(`{"leisure": "park", "area": "large"}`), Source: "osm", }, { ID: "park-small", Name: "Small Park", FeatureType: domain.GeographicalFeatureTypeGreenSpace, OSMType: "way", OSMID: "1002", Properties: datatypes.JSON(`{"leisure": "park", "area": "small"}`), Source: "osm", }, } for i, gs := range greenSpaces { err := suite.geoRepo.Create(context.Background(), gs) suite.Require().NoError(err) // Ensure the DB has a geometry for each green space so radius queries // return them during tests. Place them near 52.5200,13.4050 with small offsets lat := 52.5200 + float64(i)*0.001 lng := 13.4050 + float64(i)*0.001 // Update raw geometry via SQL (PostGIS required in test DB template) if err := suite.db.Exec(`UPDATE geographical_features SET geometry = ST_SetSRID(ST_MakePoint(?::double precision, ?::double precision), 4326) WHERE id = ?`, lng, lat, gs.ID).Error; err != nil { // If updating geometry fails (e.g., PostGIS not available), continue — tests will adapt suite.T().Logf("warning: could not set geometry for test green space %s: %v", gs.ID, err) } } } func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_HighImpact() { sites := suite.setupTestSites() score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), sites[0].Latitude, sites[0].Longitude) assert.NoError(suite.T(), err) assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), score.OverallScore, 10.0) // High impact sites should have lower scores assert.Less(suite.T(), score.OverallScore, 5.0) } func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_LowImpact() { sites := suite.setupTestSites() score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), sites[1].Latitude, sites[1].Longitude) assert.NoError(suite.T(), err) assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), score.OverallScore, 10.0) // Low impact sites should have higher scores assert.Greater(suite.T(), score.OverallScore, 5.0) } func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_EcoFriendly() { sites := suite.setupTestSites() score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), sites[2].Latitude, sites[2].Longitude) assert.NoError(suite.T(), err) assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), score.OverallScore, 10.0) // Eco-friendly sites should have highest scores assert.Greater(suite.T(), score.OverallScore, 7.0) } func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateFacilityEnvironmentalScore_NonExistentSite() { // For non-existent sites, we test with coordinates directly since the method takes coordinates _, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), 0.0, 0.0) // This should work since it doesn't depend on site existence, just coordinates assert.NoError(suite.T(), err) } func (suite *EnvironmentalImpactServiceTestSuite) TestCalculateSiteEnvironmentalScore_DefaultScore() { // Create a site without environmental impact data site := &domain.Site{ ID: "site-no-data", Name: "Site Without Data", Latitude: 52.5500, Longitude: 13.4350, SiteType: domain.SiteTypeMixed, OwnerOrganizationID: "org-env-test", EnvironmentalImpact: "", // No environmental data } err := suite.siteRepo.Create(context.Background(), site) suite.Require().NoError(err) score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), site.Latitude, site.Longitude) assert.NoError(suite.T(), err) assert.GreaterOrEqual(suite.T(), score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), score.OverallScore, 10.0) // Sites without data should get a default score around 5.0 assert.InDelta(suite.T(), 5.0, score.OverallScore, 2.0) } func (suite *EnvironmentalImpactServiceTestSuite) TestEnvironmentalScoreCalculation_Components() { // Test the scoring algorithm with different site configurations testCases := []struct { name string environmentalImpact string expectedMin float64 expectedMax float64 }{ {"High Impact", "high_impact", 0.0, 3.0}, {"Low Impact", "low_impact", 4.0, 7.0}, {"Eco Friendly", "eco_friendly", 7.0, 10.0}, {"Unknown", "unknown_type", 3.0, 6.0}, } for i, tc := range testCases { suite.T().Run(tc.name, func(t *testing.T) { site := &domain.Site{ ID: "test-site-" + tc.name, Name: tc.name + " Site", Latitude: 52.5200 + float64(i)*0.01, Longitude: 13.4050 + float64(i)*0.01, SiteType: domain.SiteTypeIndustrial, OwnerOrganizationID: "org-env-test", EnvironmentalImpact: tc.environmentalImpact, } err := suite.siteRepo.Create(context.Background(), site) assert.NoError(t, err) // Quick check: our repository should find the site by ID got, gerr := suite.siteRepo.GetByID(context.Background(), site.ID) assert.NoError(t, gerr) assert.Equal(t, tc.environmentalImpact, got.EnvironmentalImpact, "site EnvironmentalImpact persisted correctly") score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), site.Latitude, site.Longitude) assert.NoError(t, err) assert.GreaterOrEqual(t, score.OverallScore, tc.expectedMin, "Score should be >= %f for %s", tc.expectedMin, tc.name) assert.LessOrEqual(t, score.OverallScore, tc.expectedMax, "Score should be <= %f for %s", tc.expectedMax, tc.name) }) } } func (suite *EnvironmentalImpactServiceTestSuite) TestEnvironmentalScoreCalculation_ScoreNormalization() { // Test that scores are properly normalized to 0-10 range sites := suite.setupTestSites() for _, site := range sites { score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), site.Latitude, site.Longitude) suite.T().Run("ScoreNormalization_"+site.ID, func(t *testing.T) { assert.NoError(t, err) assert.GreaterOrEqual(t, score.OverallScore, 0.0, "Score should be >= 0") assert.LessOrEqual(t, score.OverallScore, 10.0, "Score should be <= 10") }) } } func (suite *EnvironmentalImpactServiceTestSuite) TestGreenSpaceStatistics_IncludesExpectedFields() { suite.setupTestGreenSpaces() // Test that green spaces are created and can be retrieved greenSpaces, err := suite.geoRepo.GetGreenSpacesWithinRadius(context.Background(), 52.5200, 13.4050, 10.0) assert.NoError(suite.T(), err) // Should have at least 2 green spaces from our setup assert.GreaterOrEqual(suite.T(), len(greenSpaces), 2, "Should have at least 2 green spaces") } func (suite *EnvironmentalImpactServiceTestSuite) TestEnvironmentalImpactService_HandlesEmptyDatabase() { // Test that CalculateFacilityEnvironmentalScore works even with coordinates not near green spaces score, err := suite.envSvc.CalculateFacilityEnvironmentalScore(context.Background(), 0.0, 0.0) assert.NoError(suite.T(), err) // Should work with any coordinates assert.NotNil(suite.T(), score) // Test that green space queries work with empty database greenSpaces, err := suite.geoRepo.GetGreenSpacesWithinRadius(context.Background(), 52.5200, 13.4050, 1.0) assert.NoError(suite.T(), err) assert.Equal(suite.T(), 0, len(greenSpaces)) }