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 FacilityLocationOptimizerTestSuite struct { suite.Suite db *gorm.DB siteRepo domain.SiteRepository geoRepo domain.GeographicalFeatureRepository geoSvc *service.GeospatialService spatialMatcher *service.SpatialResourceMatcher envSvc *service.EnvironmentalImpactService transportSvc *service.TransportationService geoCalc geospatial.Calculator optimizer *service.FacilityLocationOptimizer } func (suite *FacilityLocationOptimizerTestSuite) SetupTest() { suite.db = testutils.SetupTestDBWithTestcontainers(suite.T()) suite.siteRepo = repository.NewSiteRepository(suite.db) suite.geoRepo = repository.NewGeographicalFeatureRepository(suite.db) suite.geoSvc = service.NewGeospatialService(suite.db, suite.geoRepo) suite.geoCalc = geospatial.NewCalculatorWithDefaults() suite.transportSvc = service.NewTransportationService(suite.geoCalc) suite.spatialMatcher = service.NewSpatialResourceMatcher( suite.geoRepo, suite.siteRepo, nil, // resourceFlowRepo - not needed for basic tests suite.geoSvc, suite.transportSvc, suite.geoCalc, ) suite.envSvc = service.NewEnvironmentalImpactService(suite.geoRepo, suite.siteRepo, suite.geoSvc, suite.geoCalc) suite.optimizer = service.NewFacilityLocationOptimizer( suite.geoRepo, suite.siteRepo, suite.geoSvc, suite.spatialMatcher, suite.envSvc, suite.transportSvc, ) // Create test organization org := &domain.Organization{ID: "org-facility-test", Name: "Facility Test Organization"} err := repository.NewOrganizationRepository(suite.db).Create(context.Background(), org) suite.Require().NoError(err) } func TestFacilityLocationOptimizer(t *testing.T) { suite.Run(t, new(FacilityLocationOptimizerTestSuite)) } func (suite *FacilityLocationOptimizerTestSuite) TestNewFacilityLocationOptimizer() { assert.NotNil(suite.T(), suite.optimizer) } func (suite *FacilityLocationOptimizerTestSuite) setupTestSites() []*domain.Site { sites := []*domain.Site{ { ID: "site-industrial-good", Name: "Good Industrial Site", Latitude: 52.5200, Longitude: 13.4050, SiteType: domain.SiteTypeIndustrial, OwnerOrganizationID: "org-facility-test", EnvironmentalImpact: "low_impact", AvailableUtilities: datatypes.JSON(`["electricity", "gas", "water", "rail_access"]`), ParkingSpaces: 50, LoadingDocks: 5, FloorAreaM2: 5000.0, }, { ID: "site-office-poor", Name: "Poor Office Site", Latitude: 52.5300, Longitude: 13.4150, SiteType: domain.SiteTypeOffice, OwnerOrganizationID: "org-facility-test", EnvironmentalImpact: "high_impact", AvailableUtilities: datatypes.JSON(`["electricity"]`), ParkingSpaces: 10, LoadingDocks: 1, FloorAreaM2: 1000.0, }, { ID: "site-commercial-excellent", Name: "Excellent Commercial Site", Latitude: 52.5400, Longitude: 13.4250, SiteType: domain.SiteTypeRetail, OwnerOrganizationID: "org-facility-test", EnvironmentalImpact: "eco_friendly", AvailableUtilities: datatypes.JSON(`["electricity", "gas", "water", "heating", "cooling", "pipeline_access"]`), ParkingSpaces: 100, LoadingDocks: 10, FloorAreaM2: 10000.0, }, } for _, site := range sites { err := suite.siteRepo.Create(context.Background(), site) suite.Require().NoError(err) } return sites } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_BasicOptimization() { suite.setupTestSites() criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{domain.TypeHeat}, ResourceRadiusKm: 50.0, MaxTransportCost: 200.0, MinEnvironmentalScore: 0.0, RequiredUtilities: []string{"electricity"}, MinFloorAreaM2: 1000.0, MaxResults: 5, Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) assert.NotEmpty(suite.T(), results) assert.LessOrEqual(suite.T(), len(results), criteria.MaxResults) // Verify results are sorted by score (highest first) for i := 0; i < len(results)-1; i++ { assert.GreaterOrEqual(suite.T(), results[i].Score.OverallScore, results[i+1].Score.OverallScore) } // Verify all results meet minimum criteria for _, result := range results { assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0) // Max possible score assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, criteria.MinEnvironmentalScore) } } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_WithUtilityRequirements() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{domain.TypeHeat}, ResourceRadiusKm: 100.0, MaxTransportCost: 500.0, MinEnvironmentalScore: 0.0, RequiredUtilities: []string{"gas", "water", "rail_access"}, // Very specific requirements MinFloorAreaM2: 1500.0, MaxResults: 10, Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) assert.NotEmpty(suite.T(), results) // Check that results have reasonable scores for _, result := range results { assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0) } } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_EnvironmentalFiltering() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{domain.TypeHeat}, ResourceRadiusKm: 100.0, MaxTransportCost: 500.0, MinEnvironmentalScore: 7.0, // High environmental requirement RequiredUtilities: []string{"electricity"}, MinFloorAreaM2: 1000.0, MaxResults: 10, Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) // Only sites meeting environmental criteria should be returned for _, result := range results { assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, 7.0, "All results should meet environmental criteria") } } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_SiteTypeFiltering() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{domain.TypeHeat}, ResourceRadiusKm: 100.0, MaxTransportCost: 500.0, MinEnvironmentalScore: 0.0, RequiredUtilities: []string{"electricity"}, MinFloorAreaM2: 1000.0, MaxResults: 10, Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) // Check that results have reasonable scores for _, result := range results { assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0) } } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_NoMatchingSites() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{domain.TypeHeat}, ResourceRadiusKm: 100.0, MaxTransportCost: 500.0, MinEnvironmentalScore: 9.5, // Very high requirement RequiredUtilities: []string{"nonexistent_utility"}, // Impossible requirement MinFloorAreaM2: 1000.0, MaxResults: 10, Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) assert.Empty(suite.T(), results, "No sites should match impossible criteria") } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_EmptyCriteria() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{}, ResourceRadiusKm: 0.0, MaxTransportCost: 0.0, MinEnvironmentalScore: 0.0, RequiredUtilities: []string{}, MinFloorAreaM2: 1000.0, MaxResults: 10, Weights: service.DefaultWeights, // No limit } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) // Check that we get some results assert.Greater(suite.T(), len(results), 0, "Should return some results") } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_MaxResults() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{}, ResourceRadiusKm: 100.0, MaxTransportCost: 1000.0, MinEnvironmentalScore: 0.0, RequiredUtilities: []string{}, MinFloorAreaM2: 1000.0, MaxResults: 2, // Limit to 2 results Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) assert.Len(suite.T(), results, 2, "Should return exactly MaxResults items") } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_ScoreCalculation() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{domain.TypeHeat}, ResourceRadiusKm: 50.0, MaxTransportCost: 200.0, MinEnvironmentalScore: 3.0, RequiredUtilities: []string{"electricity"}, MinFloorAreaM2: 1000.0, MaxResults: 10, Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) for _, result := range results { // Verify score components are present assert.GreaterOrEqual(suite.T(), result.Score.TransportationScore, 0.0) assert.LessOrEqual(suite.T(), result.Score.TransportationScore, 10.0) assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, 0.0) assert.LessOrEqual(suite.T(), result.Score.EnvironmentalScore, 10.0) assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0) assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0) } } func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_Explanations() { criteria := service.LocationCriteria{ RequiredResources: []domain.ResourceType{domain.TypeHeat}, ResourceRadiusKm: 50.0, MaxTransportCost: 200.0, MinEnvironmentalScore: 5.0, RequiredUtilities: []string{"electricity", "gas"}, MinFloorAreaM2: 1000.0, MaxResults: 10, Weights: service.DefaultWeights, } results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria) assert.NoError(suite.T(), err) // Check that results have valid scores for _, result := range results { assert.NotNil(suite.T(), result.Score, "Result should have a score") assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0) } }