package bootstrap

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/testsupport"
	"context"
	"log"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/testcontainers/testcontainers-go"
	postgres_module "github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"
	gorm_postgres "gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func TestSeedTenantCSVDefinesWorksmobileDomainClassTenants(t *testing.T) {
	configs, err := loadSeedTenantConfigs()
	if err != nil {
		t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
	}

	expected := []struct {
		name       string
		slug       string
		tenantType string
		parentSlug string
		domains    []string
	}{
		{
			name:       "한맥가족",
			slug:       "hanmac-family",
			tenantType: domain.TenantTypeCompanyGroup,
		},
		{
			name:       "삼안",
			slug:       "saman",
			tenantType: domain.TenantTypeCompany,
			parentSlug: "hanmac-family",
			domains:    []string{"samaneng.com"},
		},
		{
			name:       "한맥기술",
			slug:       "hanmac",
			tenantType: domain.TenantTypeCompany,
			parentSlug: "hanmac-family",
			domains:    []string{"hanmaceng.co.kr"},
		},
		{
			name:       "총괄기획&기술개발센터",
			slug:       "gpdtdc",
			tenantType: domain.TenantTypeCompany,
			parentSlug: "hanmac-family",
			domains:    []string{"baroncs.co.kr"},
		},
		{
			name:       "바론그룹",
			slug:       "baron-group",
			tenantType: domain.TenantTypeCompanyGroup,
			parentSlug: "hanmac-family",
			domains:    []string{"brsw.kr"},
		},
		{
			name:       "(주)장헌",
			slug:       "jangheon",
			tenantType: domain.TenantTypeCompany,
			parentSlug: "baron-group",
			domains:    []string{"jangheon.com"},
		},
		{
			name:       "장헌산업",
			slug:       "jangheon-sanup",
			tenantType: domain.TenantTypeCompany,
			parentSlug: "baron-group",
			domains:    []string{"jangheon.co.kr"},
		},
		{
			name:       "한라산업개발",
			slug:       "halla",
			tenantType: domain.TenantTypeCompany,
			parentSlug: "hanmac-family",
			domains:    []string{"hallasanup.com"},
		},
		{
			name:       "(주)피티씨",
			slug:       "ptc",
			tenantType: domain.TenantTypeCompany,
			parentSlug: "baron-group",
			domains:    []string{"pre-cast.co.kr"},
		},
		{
			name:       "Personal",
			slug:       "personal",
			tenantType: domain.TenantTypePersonal,
		},
	}

	if len(configs) < len(expected) {
		t.Fatalf("expected at least %d seed tenants, got %d", len(expected), len(configs))
	}

	wantFamilyChildOrder := []string{
		"gpdtdc",
		"saman",
		"hanmac",
		"baron-group",
		"halla",
	}
	policyFamilyChildSlugs := map[string]bool{}
	for _, slug := range wantFamilyChildOrder {
		policyFamilyChildSlugs[slug] = true
	}
	gotFamilyChildOrder := make([]string, 0, len(wantFamilyChildOrder))
	for _, config := range configs {
		if config.ParentSlug == "hanmac-family" && policyFamilyChildSlugs[config.Slug] {
			gotFamilyChildOrder = append(gotFamilyChildOrder, config.Slug)
		}
	}
	if len(gotFamilyChildOrder) != len(wantFamilyChildOrder) {
		t.Fatalf("hanmac-family child order = %#v, want %#v", gotFamilyChildOrder, wantFamilyChildOrder)
	}
	for i, wantSlug := range wantFamilyChildOrder {
		if gotFamilyChildOrder[i] != wantSlug {
			t.Fatalf("hanmac-family child order[%d] = %q, want %q", i, gotFamilyChildOrder[i], wantSlug)
		}
	}

	configBySlug := make(map[string]InitialTenantConfig, len(configs))
	for _, config := range configs {
		configBySlug[config.Slug] = config
	}

	for _, want := range expected {
		got, ok := configBySlug[want.slug]
		if !ok {
			t.Fatalf("tenant slug %q not found in seed configs", want.slug)
		}
		if got.Name != want.name {
			t.Fatalf("tenant[%s] name = %q, want %q", want.slug, got.Name, want.name)
		}
		if got.Slug != want.slug {
			t.Fatalf("tenant[%s] slug = %q, want %q", want.slug, got.Slug, want.slug)
		}
		if got.Type != want.tenantType {
			t.Fatalf("tenant[%s] type = %q, want %q", want.slug, got.Type, want.tenantType)
		}
		if got.ParentSlug != want.parentSlug {
			t.Fatalf("tenant[%s] parent slug = %q, want %q", want.slug, got.ParentSlug, want.parentSlug)
		}
		if len(got.Domains) != len(want.domains) {
			t.Fatalf("tenant[%s] domains = %#v, want %#v", want.slug, got.Domains, want.domains)
		}
		for j, wantDomain := range want.domains {
			if got.Domains[j] != wantDomain {
				t.Fatalf("tenant[%s] domain[%d] = %q, want %q", want.slug, j, got.Domains[j], wantDomain)
			}
		}
	}
}

func TestNormalizeSeedTenantTypeAllowsOrganization(t *testing.T) {
	if got := normalizeSeedTenantType("organization"); got != domain.TenantTypeOrganization {
		t.Fatalf("normalizeSeedTenantType(organization) = %q, want %q", got, domain.TenantTypeOrganization)
	}
}

func TestLoadSeedTenantConfigsUsesConfiguredCSVPath(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "seed-tenant.csv")
	csv := "name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync\n" +
		"Root,COMPANY_GROUP,,root,Root memo,,,,\n" +
		"Child,USER_GROUP,root,child,Child memo,child.example.com,private,팀,no\n"
	if err := os.WriteFile(path, []byte(csv), 0o600); err != nil {
		t.Fatalf("failed to write seed csv: %v", err)
	}
	t.Setenv(seedTenantCSVPathEnv, path)

	configs, err := loadSeedTenantConfigs()
	if err != nil {
		t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
	}
	if len(configs) != 2 {
		t.Fatalf("expected 2 configs, got %d", len(configs))
	}
	if configs[1].ParentSlug != "root" {
		t.Fatalf("child parent slug = %q, want root", configs[1].ParentSlug)
	}
	if len(configs[1].Domains) != 1 || configs[1].Domains[0] != "child.example.com" {
		t.Fatalf("child domains = %#v, want child.example.com", configs[1].Domains)
	}
	if configs[1].Config["visibility"] != "private" {
		t.Fatalf("child visibility = %#v, want private", configs[1].Config["visibility"])
	}
	if configs[1].Config["orgUnitType"] != "팀" {
		t.Fatalf("child orgUnitType = %#v, want 팀", configs[1].Config["orgUnitType"])
	}
	if configs[1].Config["worksmobileExcluded"] != true {
		t.Fatalf("child worksmobileExcluded = %#v, want true", configs[1].Config["worksmobileExcluded"])
	}
}

func TestSeedTenantCSVDefinesMHDAsPrivateUserGroup(t *testing.T) {
	configs, err := loadSeedTenantConfigs()
	if err != nil {
		t.Fatalf("loadSeedTenantConfigs returned error: %v", err)
	}

	configBySlug := make(map[string]InitialTenantConfig, len(configs))
	for _, config := range configs {
		configBySlug[config.Slug] = config
	}

	mhd, ok := configBySlug["mhd"]
	if !ok {
		t.Fatal("mhd seed tenant not found")
	}
	if mhd.Type != domain.TenantTypeUserGroup {
		t.Fatalf("mhd type = %q, want %q", mhd.Type, domain.TenantTypeUserGroup)
	}
	if mhd.Config["visibility"] != "private" {
		t.Fatalf("mhd visibility = %#v, want private", mhd.Config["visibility"])
	}
	if mhd.Config["worksmobileExcluded"] != true {
		t.Fatalf("mhd worksmobileExcluded = %#v, want true", mhd.Config["worksmobileExcluded"])
	}
}

func TestIsSeedTenantSlugUsesConfiguredCSVPath(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "seed-tenant.csv")
	csv := "name,type,parent_tenant_slug,slug,memo,email_domain\n" +
		"Root,COMPANY_GROUP,,protected-root,Root memo,\n"
	if err := os.WriteFile(path, []byte(csv), 0o600); err != nil {
		t.Fatalf("failed to write seed csv: %v", err)
	}
	t.Setenv(seedTenantCSVPathEnv, path)

	if !IsSeedTenantSlug("protected-root") {
		t.Fatal("protected-root must be detected as seed tenant")
	}
	if IsSeedTenantSlug("normal-tenant") {
		t.Fatal("normal-tenant must not be detected as seed tenant")
	}
}

func TestFilterMissingSeedTenantConfigsSkipsExistingSlugs(t *testing.T) {
	configs := []InitialTenantConfig{
		{TenantID: "existing-root-id", Name: "Existing Root", Slug: "existing-root"},
		{Name: "Missing Child", Slug: "missing-child", ParentSlug: "existing-root"},
		{TenantID: "existing-child-id", Name: "Existing Child", Slug: "existing-child", ParentSlug: "existing-root"},
		{TenantID: "existing-other-id", Name: "Conflicting ID", Slug: "new-slug"},
	}
	existingSlugs := map[string]bool{
		"existing-root":  true,
		"existing-child": true,
	}
	existingIDs := map[string]bool{
		"existing-root-id":  true,
		"existing-child-id": true,
		"existing-other-id": true,
	}

	filtered := filterMissingSeedTenantConfigs(configs, existingSlugs, existingIDs)

	if len(filtered) != 1 {
		t.Fatalf("filtered count = %d, want 1: %#v", len(filtered), filtered)
	}
	if filtered[0].Slug != "missing-child" {
		t.Fatalf("filtered slug = %q, want missing-child", filtered[0].Slug)
	}
}

func TestSeedTenantsCreatesMissingSeedRowsAndRepairsExistingSeedSlug(t *testing.T) {
	if !testsupport.DockerAvailable() {
		t.Skip("Docker provider is unavailable in this environment")
	}

	ctx := context.Background()
	postgresContainer, err := postgres_module.Run(ctx,
		"postgres:16-alpine",
		postgres_module.WithDatabase("testdb"),
		postgres_module.WithUsername("user"),
		postgres_module.WithPassword("password"),
		testcontainers.WithWaitStrategy(
			wait.ForLog("database system is ready to accept connections").
				WithOccurrence(2).
				WithStartupTimeout(30*time.Second),
		),
	)
	if err != nil {
		t.Fatalf("failed to start postgres container: %v", err)
	}
	t.Cleanup(func() {
		if err := postgresContainer.Terminate(ctx); err != nil {
			log.Printf("failed to terminate postgres container: %v", err)
		}
	})

	connStr, err := postgresContainer.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		t.Fatalf("failed to get postgres connection string: %v", err)
	}
	db, err := gorm.Open(gorm_postgres.Open(connStr), &gorm.Config{})
	if err != nil {
		t.Fatalf("failed to open postgres connection: %v", err)
	}
	if err := db.AutoMigrate(&domain.Tenant{}, &domain.TenantDomain{}, &domain.KetoOutbox{}); err != nil {
		t.Fatalf("failed to migrate seed test tables: %v", err)
	}

	existingRoot := domain.Tenant{
		ID:          "00000000-0000-0000-0000-000000000001",
		Name:        "Existing Root Name",
		Slug:        "existing-root",
		Type:        domain.TenantTypeCompanyGroup,
		Description: "manual tenant must not be overwritten",
		Status:      domain.TenantStatusActive,
	}
	nonSeedTenant := domain.Tenant{
		ID:     "00000000-0000-0000-0000-000000000002",
		Name:   "Manual Tenant",
		Slug:   "manual-tenant",
		Type:   domain.TenantTypeCompany,
		Status: domain.TenantStatusActive,
	}
	existingSeedTenantWithTypoSlug := domain.Tenant{
		ID:          "5a03efd2-e62f-4243-800d-58334bf48b2f",
		Name:        "한라산업개발",
		Slug:        "hanlla",
		Type:        domain.TenantTypeCompany,
		Description: "seed tenant with a typo slug must be repaired by UUID",
		Status:      domain.TenantStatusActive,
		ParentID:    &existingRoot.ID,
	}
	if err := db.Create(&existingRoot).Error; err != nil {
		t.Fatalf("failed to create existing root tenant: %v", err)
	}
	if err := db.Create(&nonSeedTenant).Error; err != nil {
		t.Fatalf("failed to create non-seed tenant: %v", err)
	}
	if err := db.Create(&existingSeedTenantWithTypoSlug).Error; err != nil {
		t.Fatalf("failed to create existing seed tenant with typo slug: %v", err)
	}

	dir := t.TempDir()
	path := filepath.Join(dir, "seed-tenant.csv")
	csv := "id,name,type,parent_tenant_slug,slug,memo,email_domain\n" +
		"10000000-0000-0000-0000-000000000001,Seed Root Name,COMPANY_GROUP,,existing-root,seed must be skipped,\n" +
		"5a03efd2-e62f-4243-800d-58334bf48b2f,한라산업개발,COMPANY,existing-root,halla,seed typo slug must be repaired,hallasanup.com\n" +
		"10000000-0000-0000-0000-000000000002,Missing Child,COMPANY,existing-root,missing-child,created from seed,child.example.com\n"
	if err := os.WriteFile(path, []byte(csv), 0o600); err != nil {
		t.Fatalf("failed to write seed csv: %v", err)
	}
	t.Setenv(seedTenantCSVPathEnv, path)

	if err := SeedTenants(db); err != nil {
		t.Fatalf("SeedTenants returned error: %v", err)
	}

	var root domain.Tenant
	if err := db.First(&root, "slug = ?", "existing-root").Error; err != nil {
		t.Fatalf("failed to load existing root after seed: %v", err)
	}
	if root.ID != existingRoot.ID {
		t.Fatalf("existing root ID = %q, want %q", root.ID, existingRoot.ID)
	}
	if root.Name != existingRoot.Name {
		t.Fatalf("existing root name = %q, want untouched %q", root.Name, existingRoot.Name)
	}

	var manual domain.Tenant
	if err := db.First(&manual, "id = ?", nonSeedTenant.ID).Error; err != nil {
		t.Fatalf("failed to load non-seed tenant after seed: %v", err)
	}
	if manual.Slug != nonSeedTenant.Slug {
		t.Fatalf("non-seed tenant slug = %q, want untouched %q", manual.Slug, nonSeedTenant.Slug)
	}

	var repairedSeed domain.Tenant
	if err := db.First(&repairedSeed, "id = ?", existingSeedTenantWithTypoSlug.ID).Error; err != nil {
		t.Fatalf("failed to load existing seed tenant after seed: %v", err)
	}
	if repairedSeed.Slug != "halla" {
		t.Fatalf("existing seed tenant slug = %q, want halla", repairedSeed.Slug)
	}

	var child domain.Tenant
	if err := db.Preload("Domains").First(&child, "slug = ?", "missing-child").Error; err != nil {
		t.Fatalf("missing seed child was not created: %v", err)
	}
	if child.ParentID == nil || *child.ParentID != existingRoot.ID {
		t.Fatalf("child parent ID = %v, want %q", child.ParentID, existingRoot.ID)
	}
	if len(child.Domains) != 1 || child.Domains[0].Domain != "child.example.com" {
		t.Fatalf("child domains = %#v, want child.example.com", child.Domains)
	}

	var rootCount int64
	if err := db.Model(&domain.Tenant{}).Where("slug = ?", "existing-root").Count(&rootCount).Error; err != nil {
		t.Fatalf("failed to count existing root rows: %v", err)
	}
	if rootCount != 1 {
		t.Fatalf("existing-root row count = %d, want 1", rootCount)
	}
}
