package loader

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
)

type mockSource struct {
	conf *config.RootConfig
	err  error
}

func (s *mockSource) Load(_ context.Context, pre *config.RootConfig) (*config.RootConfig, error) {
	if s.err != nil {
		return nil, s.err
	}
	return s.conf, nil
}

func TestConfigurationLoader(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Configuration Loader Suite", test.ReporterConfig())
}

var _ = Describe("[ConfigurationLoader]", func() {
	Describe("ConfigurationSourceFile", func() {
		It("loads test infra tenant IDs", func() {
			configPath := filepath.Join(GinkgoT().TempDir(), "config.yaml")
			err := os.WriteFile(configPath, []byte(`
services:
  tenant:
    testInfraTenantIDs:
      - 1372813089209272198
      - 1369847559692509640
`), 0o600)
			Expect(err).ToNot(HaveOccurred())

			loaded, err := NewConfigurationSourceFile(configPath).Load(context.Background(), nil)
			Expect(err).ToNot(HaveOccurred())
			Expect(loaded.Services.Tenant.TestInfraTenantIDs).To(Equal([]uint64{
				1372813089209272198,
				1369847559692509640,
			}))
		})

		It("loads premium upgrade approval nodes", func() {
			configPath := filepath.Join(GinkgoT().TempDir(), "config.yaml")
			err := os.WriteFile(configPath, []byte(`
services:
  premiumUpgrade:
    approval:
      nodes:
        - name: Kernel approval
          components:
            - LOGICAL_CLUSTER/tidb
          userGroup: cloud-operation-release-kernel
          method: ANY
          selfApprove: true
`), 0o600)
			Expect(err).ToNot(HaveOccurred())

			loaded, err := NewConfigurationSourceFile(configPath).Load(context.Background(), nil)
			Expect(err).ToNot(HaveOccurred())
			Expect(loaded.Services.PremiumUpgrade.Approval.Nodes).To(HaveLen(1))
			node := loaded.Services.PremiumUpgrade.Approval.Nodes[0]
			Expect(node.Name).To(Equal("Kernel approval"))
			Expect(node.Components).To(Equal([]string{"LOGICAL_CLUSTER/tidb"}))
			Expect(node.UserGroup).To(Equal("cloud-operation-release-kernel"))
			Expect(string(node.Method)).To(Equal("ANY"))
			Expect(node.SelfApprove).To(BeTrue())
		})
	})

	Describe("Load", func() {
		It("merges configs from sources", func() {
			base := validRootConfig()
			base.Log.Level = 0

			overlay := validRootConfig()
			overlay.Log.Level = 2
			overlay.Asset.BucketName = "overlay-bucket"

			loader := &ConfigurationLoader{
				sources: []ConfigurationSource{
					&ConfigurationSourceDefault{},
					&mockSource{conf: base},
					&mockSource{conf: overlay},
				},
				skipValidate: true,
			}

			conf, err := loader.Load(context.Background())
			Expect(err).To(BeNil())
			Expect(conf.Log.Level).To(Equal(int16(2)))
			Expect(conf.Asset.BucketName).To(Equal(overlay.Asset.BucketName))
		})

		It("preserves explicit zero active EMEA tenant min cluster count", func() {
			zero := 0
			fileConf := validRootConfig()
			fileConf.Services.ActiveEMEATenant.MinActiveClusterCount = &zero

			loader := &ConfigurationLoader{
				sources: []ConfigurationSource{
					&ConfigurationSourceDefault{},
					&mockSource{conf: fileConf},
				},
				skipValidate: true,
			}

			conf, err := loader.Load(context.Background())
			Expect(err).To(BeNil())
			Expect(conf.Services.ActiveEMEATenant.MinActiveClusterCount).ToNot(BeNil())
			Expect(*conf.Services.ActiveEMEATenant.MinActiveClusterCount).To(Equal(0))
		})

		It("preserves an explicitly disabled premium upgrade dummy service", func() {
			configPath := filepath.Join(GinkgoT().TempDir(), "config.yaml")
			err := os.WriteFile(configPath, []byte(`
services:
  premiumUpgrade:
    dummy: false
`), 0o600)
			Expect(err).ToNot(HaveOccurred())

			loader := &ConfigurationLoader{
				sources: []ConfigurationSource{
					&ConfigurationSourceDefault{},
					NewConfigurationSourceFile(configPath),
				},
				skipValidate: true,
			}

			conf, err := loader.Load(context.Background())
			Expect(err).To(BeNil())
			Expect(conf.Services.PremiumUpgrade.Dummy).ToNot(BeNil())
			Expect(pointer.Deref(conf.Services.PremiumUpgrade.Dummy, true)).To(BeFalse())
		})

		It("returns error when source fails", func() {
			expectedErr := errors.New("boom")
			loader := &ConfigurationLoader{
				sources: []ConfigurationSource{
					&mockSource{conf: validRootConfig()},
					&mockSource{err: expectedErr},
				},
				skipValidate: true,
			}

			conf, err := loader.Load(context.Background())
			Expect(err).To(MatchError(expectedErr))
			Expect(conf).To(BeNil())
		})

		It("returns validation error for invalid config", func() {
			invalid := validRootConfig()
			invalid.Asset.BucketName = ""

			loader := &ConfigurationLoader{
				sources: []ConfigurationSource{
					&mockSource{conf: invalid},
				},
			}

			conf, err := loader.Load(context.Background())
			Expect(err).To(HaveOccurred())
			Expect(conf).To(BeNil())
		})

		It("passes validation when dummy services are enabled", func() {
			confWithDummy := validRootConfig()

			confWithDummy.Services.ClusterMaintenance.Dummy = true
			confWithDummy.Services.ClusterMaintenance.NotifyChatID = ""
			confWithDummy.Services.ClusterMaintenance.EmailFromAddress = ""
			confWithDummy.Services.ClusterMaintenance.BeforeTemplateID = ""
			confWithDummy.Services.ClusterMaintenance.StartedTemplateID = ""
			confWithDummy.Services.ClusterMaintenance.CompletedTemplateID = ""

			confWithDummy.Services.Dashboard.Dummy = true
			confWithDummy.Services.Dashboard.EEPrometheusEndpoint = ""
			confWithDummy.Services.Dashboard.BasePrometheusEndpoint = ""

			confWithDummy.Services.Alert.Dummy = true
			confWithDummy.Services.Alert.JiraProject = ""

			confWithDummy.Services.K8sUpgrade.Dummy = true
			confWithDummy.Services.K8sUpgrade.DetailLinkBaseURL = ""

			loader := &ConfigurationLoader{
				sources: []ConfigurationSource{
					&mockSource{conf: confWithDummy},
				},
			}

			loaded, err := loader.Load(context.Background())
			Expect(err).To(BeNil())
			Expect(loaded).NotTo(BeNil())
		})

		It("fails validation when dummy services are disabled and required fields are missing", func() {
			type testCase struct {
				name   string
				mutate func(*config.RootConfig)
			}

			cases := []testCase{
				{
					name: "cluster maintenance requires notify chat id",
					mutate: func(c *config.RootConfig) {
						c.Services.ClusterMaintenance.Dummy = false
						c.Services.ClusterMaintenance.NotifyChatID = ""
					},
				},
				{
					name: "dashboard requires ee prometheus endpoint",
					mutate: func(c *config.RootConfig) {
						c.Services.Dashboard.Dummy = false
						c.Services.Dashboard.EEPrometheusEndpoint = ""
					},
				},
				{
					name: "alert requires jira project",
					mutate: func(c *config.RootConfig) {
						c.Services.Alert.Dummy = false
						c.Services.Alert.JiraProject = ""
					},
				},
				{
					name: "k8s upgrade requires detail link base url",
					mutate: func(c *config.RootConfig) {
						c.Services.K8sUpgrade.Dummy = false
						c.Services.K8sUpgrade.DetailLinkBaseURL = ""
					},
				},
			}

			for _, tc := range cases {
				By(tc.name)

				invalid := validRootConfig()
				tc.mutate(invalid)

				loader := &ConfigurationLoader{
					sources: []ConfigurationSource{
						&mockSource{conf: invalid},
					},
				}

				loaded, err := loader.Load(context.Background())
				Expect(err).To(HaveOccurred())
				Expect(loaded).To(BeNil())
			}
		})
	})
})

func validRootConfig() *config.RootConfig {
	return &config.RootConfig{
		SecretSource: config.SecretSource{},
		Log:          config.LogConfig{Level: 1},
		Asset:        config.AssetConfig{BucketName: "bucket"},
		MySQL:        *dummyDatabaseConfig("main"),
		EventBus:     config.EventBusConfig{Disable: false, AppName: "devops"},
		APIServer: config.APIServerConfig{
			ListenAddr: "0.0.0.0:8080",
			Auth: config.AuthConfig{
				AuthDomain:   "domain",
				AuthAudience: "audience",
			},
		},
		GrpcServer: config.GrpcServerConfig{ListenAddr: "0.0.0.0:8081"},
		Services:   validServicesConfig(),
		Controllers: config.Controllers{
			Webhook: config.WebhookControllerConfig{S1TicketScheduleEnabled: false},
		},
		Clients: config.Clients{
			Lark: config.LarkConfig{
				Global: config.LarkCertConfig{
					AppCode:           "appcode",
					AppSecret:         "secret",
					EncryptKey:        "encrypt",
					VerificationToken: "token",
				},
				CardEventProxyKey:   "env",
				CardEventProxyAddrs: map[string]string{"dev": "dev.example.com"},
			},
			GCP:      config.GCPClientConfig{ProjectID: "project", Region: "us-west-2"},
			SendGrid: config.SendGrid{APIKey: "api-key"},
			FAD:      config.FADConfig{DataServicePublicKey: "pub", DataServicePrivateKey: "priv"},
			Authing: config.AuthingConfig{
				ManagementAccessKey: "authing-ak",
				ManagementSecretKey: "authing-sk",
			},
			Ones: config.OnesClientConfig{
				Domain: "https://ones.example.com",
				TeamID: "team",
				Token:  "ones-token",
			},
			InfraAPIServer: config.InfraAPIServerConfig{ // nolint:gosec
				KubeConfigSecretName: "infra-kubeconfig",
			},
			Jira: config.JiraConfig{
				JiraURL:                 "https://jira.example.com",
				JiraUsername:            "user",
				JiraPassword:            "password",
				LarkGroupPluginQueryURL: "https://lark.example.com/query",
				LarkGroupPluginBindURL:  "https://lark.example.com/bind",
			},
			EEServer:     config.EEServerConfig{Addr: "ee", User: "user", Password: "pass"},
			Github:       config.GithubConfig{Token: "token"},
			GithubAction: config.GithubActionConfig{Token: "token", Owner: "owner", Repo: "repo"},
			AccountServer: config.AccountServer{
				AK: "ak",
				SK: "sk",
			},
			ClinicServer: config.ClinicServer{Addr: "clinic", Token: "token"},
			AIOpsServer:  config.AIOpsServerConfig{Addr: "aiops", APIKey: "api"},
			LLM:          config.LLMConfig{BedrockToken: "bedrock", GeminiKey: "gemini"},
			OnCall:       config.OnCallConfig{Addr: "http://oncall:8080", ServiceKey: "key"},
		},
		DbaasAddr: validDbaasAddrConfig(),
	}
}

func validServicesConfig() config.Services {
	return config.Services{
		InternalToken: config.InternalTokenServiceConfig{
			DefaultTTL:    pointer.Pointer(time.Duration(15) * time.Minute),
			SigningKeyPEM: "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----",
			KeyID:         "devops-key-1",
		},
		Approval:    config.ApprovalServiceConfig{TemplatesFilepath: "/tmp/approval.yaml"},
		Release:     config.ReleaseSerivceConfig{Project: "DM", NotifyChatID: "chat"},
		Application: config.ApplicationServiceConfig{},
		ClusterCtl:  config.ClusterCtlServiceConfig{},
		ClusterMaintenance: config.ClusterMaintenanceConfig{
			NotifyChatID:        "chat",
			EmailFromAddress:    "no-reply@example.com",
			BeforeTemplateID:    "before",
			StartedTemplateID:   "started",
			CompletedTemplateID: "completed",
		},
		Monitoring: config.MonitoringServiceConfig{
			Certificate: config.CertMonitoringServiceConfig{
				ClinicToken:  "clinic",
				GrantType:    "grant",
				ClientID:     "client",
				ClientSecret: "secret",
				Audience:     "audience",
				BizType:      "biz",
			},
			IdleReminder: config.IdleReminderServiceConfig{
				WeeklyReport: config.WeeklyReportConfig{
					EmailFromAddress: "no-reply@example.com",
				},
			},
		},
		Incident: config.IncidentServiceConfig{},
		Tenant: config.TenantServiceConfig{
			CronSpec:        "0 0 * * *",
			TenantTableName: "tenant",
			O11yEndpoint:    "https://o11y.example.com",
			PrometheusPath:  "/prometheus",
		},
		Dashboard: config.DashboardServiceConfig{
			SlaDB:                  dummyDatabaseConfig("sla"),
			ServerlessSlaDB:        dummyDatabaseConfig("serverless"),
			EEPrometheusEndpoint:   "https://ee.example.com",
			BasePrometheusEndpoint: "https://base.example.com",
		},
		Github: config.GithubServiceConfig{},
		Operation: config.OperationServiceConfig{
			OperationRegistryFilepath: "/tmp/operation.yaml",
		},
		Jump: config.JumpServiceConfig{
			Console: config.JumpServiceJumpServerConsoleConfig{
				JumpServerAccessKeyID:     "ak",
				JumpServerAccessKeySecret: "sk",
			},
			JumpPod: config.JumpServiceJumpPodConfig{
				JumpPodImage:        "jump-pod-image",
				JumpPodAgentImage:   "jump-pod-agent-image",
				JumpPodRoleARN:      "arn:aws:iam::123456789012:role/jump-pod-role",
				JumpPodSharedFSAddr: "efs",
			},
			DestinationRegistryFilepath: "/tmp/dest.yaml",
			Tailscale: config.JumpServiceTailscaleClientConfig{
				TailscaleClientID:     "ts-client",
				TailscaleClientSecret: "ts-secret",
			},
		},
		Alert: config.AlertServiceConfig{JiraProject: "O11Y"},
		AutoCredit: config.AutoCreditServiceConfig{
			CronDisable:   false,
			CronSpec:      "*/5 * * * *",
			CreditAmount:  "100",
			Usage:         "poc",
			NotePrefix:    "AUTO_CREDIT_TEST",
			OperatorEmail: "fan.wang@pingcap.com",
		},
		ActiveEMEATenant: config.ActiveEMEATenantReportConfig{
			CronDisable:    false,
			CronSpec:       "0 9 * * *",
			OperatorEmail:  "xieyujie@pingcap.com",
			NotifyChatID:   "chat",
			ReportS3Prefix: "active-emea-tenant-report",
		},
		CloudConfig:  config.CloudConfigServiceConfig{},
		UserBehavior: config.UserBehaviorServiceConfig{},
		K8sUpgrade: config.K8sUpgradeServiceConfig{
			DetailLinkBaseURL: "https://detail.example.com",
		},
		RemediationTicket: config.RemediationTicketConfig{},
		Privilege:         config.PrivilegeServiceConfig{},
	}
}

func validDbaasAddrConfig() config.DbaasAddrConfig {
	return config.DbaasAddrConfig{
		CoreServiceAddr:       "core:8080",
		OpsCoreServiceAddr:    "ops-core:8080",
		MgmtServiceAddr:       "mgmt:8080",
		NextGenGlobalAddr:     "nextgen-global:10000",
		AccountServiceAddr:    "account:8080",
		BillingServiceAddr:    "billing:8080",
		ServerlessServiceAddr: "serverless:8080",
		ConfigCenterAddr:      "config-center:8080",
		DataflowServiceAddr:   "dataflow:8080",
		DataflowNgServiceAddr: "dataflow-ng:8080",
		MgmtPortalServiceAddr: "portal:8080",
		O11yServiceAddr:       "o11y:8080",
		CloudInfraMetaDB:      *dummyDatabaseConfig("cloud-infra"),
		MgmtService:           config.MgmtServiceConfig{HTTPAddr: "mgmt-http:8080", HTTPToken: "token"},
	}
}

func dummyDatabaseConfig(name string) *config.DatabaseConfig {
	return &config.DatabaseConfig{
		Host:     name + ".host",
		Port:     3306,
		User:     "user",
		Password: "password",
		Database: name + "_db",
	}
}
