package controlplane

import (
	"context"
	"testing"

	cp "github.com/tidbcloud/devops/entity/controlplane"
	"github.com/tidbcloud/devops/pkg/infra/clients/githubcli"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	apprsvcapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	depsvcapi "github.com/tidbcloud/devops/pkg/service/controlplane/api"
	"github.com/tidbcloud/devops/pkg/service/controlplane/repo"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/pkgv2/db/mock_db"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"go.uber.org/mock/gomock"
)

func newSvcForTest(dbp db.Transactioner, ghCli githubcli.Client, apprSvc apprsvcapi.ApprovalServiceAPI, r repo.Repo) *svc {
	return &svc{
		dbp:     dbp,
		ghCli:   ghCli,
		apprSvc: apprSvc,
		repo:    r,
	}
}

func TestDeploymentService(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Deployment Service Suite", test.ReporterConfig())
}

var _ = Describe("[DeploymentService]", func() {
	var fw = test.Framework()
	var mockRepo *repo.MockRepo
	var mockGHCli *githubcli.MockClient
	var mockApprSvc *apprsvcapi.MockApprovalServiceAPI
	var s *svc

	validAWSPremiumParams := map[string]string{
		"environment":        "dev",
		"region-name":        "us-west-2-premium-0",
		"aws-region":         "us-west-2",
		"aws-account":        "123456789012",
		"dataplane-accounts": "123456789012",
		"availability-zones": "usw2-az1,usw2-az2,usw2-az3",
	}

	BeforeEach(func() {
		mockRepo = repo.NewMockRepo(fw.Ctrl)
		mockGHCli = githubcli.NewMockClient(fw.Ctrl)
		mockApprSvc = apprsvcapi.NewMockApprovalServiceAPI(fw.Ctrl)
		s = newSvcForTest(mock_db.NewFakeTransProvider(), mockGHCli, mockApprSvc, mockRepo)
	})

	Describe("[CreateDeployment]", func() {
		It("should create a deployment with valid params", func() {
			mockRepo.EXPECT().Create(gomock.Any(), gomock.Any()).DoAndReturn(
				func(ctx context.Context, dep *cp.Deployment) (*cp.Deployment, error) {
					Expect(dep.Phase).To(Equal(cp.PhaseCreated))
					Expect(dep.Scenario).To(Equal(cp.ScenarioAWSPremiumRegion))
					Expect(dep.Applicant).To(Equal("test@example.com"))
					dep.ID = 1
					return dep, nil
				},
			)

			id, err := s.CreateDeployment(context.Background(), depsvcapi.CreateDeploymentParam{
				Scenario: cp.ScenarioAWSPremiumRegion,
				Params:   validAWSPremiumParams,
			}, "test@example.com")
			Expect(err).To(BeNil())
			Expect(*id).To(Equal(uint64(1)))
		})

		It("should reject unsupported scenario", func() {
			_, err := s.CreateDeployment(context.Background(), depsvcapi.CreateDeploymentParam{
				Scenario: cp.Scenario("unsupported"),
				Params:   validAWSPremiumParams,
			}, "test@example.com")
			Expect(err).ToNot(BeNil())
		})

		It("should reject missing required params", func() {
			_, err := s.CreateDeployment(context.Background(), depsvcapi.CreateDeploymentParam{
				Scenario: cp.ScenarioAWSPremiumRegion,
				Params:   map[string]string{"environment": "dev"},
			}, "test@example.com")
			Expect(err).ToNot(BeNil())
		})
	})

	Describe("[StartPreview]", func() {
		It("should dispatch preview workflow and freeze snapshot", func() {
			dep := &cp.Deployment{
				ID:       1,
				Scenario: cp.ScenarioAWSPremiumRegion,
				Phase:    cp.PhaseCreated,
				Params:   validAWSPremiumParams,
			}

			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)
			mockGHCli.EXPECT().DeployWorkflow(gomock.Any(), "deploy-aws-premium-region.yml", gomock.Any()).Return(int64(99001), "https://github.com/tidbcloud/auto-deploy/actions/runs/99001", nil)
			mockRepo.EXPECT().GetAndPatch(gomock.Any(), uint64(1), gomock.Any()).DoAndReturn(
				func(ctx context.Context, id uint64, patches ...repo.PatchDeployment) error {
					for _, p := range patches {
						if err := p(dep); err != nil {
							return err
						}
					}
					Expect(dep.Phase).To(Equal(cp.PhasePreviewRunning))
					Expect(dep.FrozenSnapshot).ToNot(BeNil())
					Expect(dep.FrozenSnapshot.WorkflowName).To(Equal("deploy-aws-premium-region.yml"))
					Expect(dep.FrozenSnapshot.WorkflowInputs["preview"]).To(Equal("true"))
					Expect(dep.PreviewMeta.RunID).To(Equal(int64(99001)))
					return nil
				},
			)

			result, err := s.StartPreview(context.Background(), 1)
			Expect(err).To(BeNil())
			Expect(result.RunID).To(Equal(int64(99001)))
			Expect(result.RunURL).To(Equal("https://github.com/tidbcloud/auto-deploy/actions/runs/99001"))
		})

		It("should reject preview from invalid phase", func() {
			dep := &cp.Deployment{
				ID:       1,
				Scenario: cp.ScenarioAWSPremiumRegion,
				Phase:    cp.PhaseApproving,
				Params:   validAWSPremiumParams,
			}

			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)

			_, err := s.StartPreview(context.Background(), 1)
			Expect(err).ToNot(BeNil())
		})
	})

	Describe("[OnPreviewComplete]", func() {
		It("should transition to approving and create approval on preview success", func() {
			dep := &cp.Deployment{
				ID:        1,
				Scenario:  cp.ScenarioAWSPremiumRegion,
				Phase:     cp.PhasePreviewRunning,
				Applicant: "test@example.com",
				Params:    validAWSPremiumParams,
				PreviewMeta: &cp.WorkflowRunMeta{Status: "pending"},
				FrozenSnapshot: &cp.FrozenSnapshot{
					WorkflowName:   "deploy-aws-premium-region.yml",
					WorkflowInputs: map[string]string{"preview": "true"},
					Params:         validAWSPremiumParams,
				},
			}

			approvalID := uint64(42)
			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)
			mockApprSvc.EXPECT().CreateApprovalFromTemplate(gomock.Any(), gomock.Any()).Return(&approvalID, nil)
			mockRepo.EXPECT().GetAndPatch(gomock.Any(), uint64(1), gomock.Any()).DoAndReturn(
				func(ctx context.Context, id uint64, patches ...repo.PatchDeployment) error {
					for _, p := range patches {
						if err := p(dep); err != nil {
							return err
						}
					}
					Expect(dep.Phase).To(Equal(cp.PhaseApproving))
					Expect(dep.ApprovalID).ToNot(BeNil())
					Expect(*dep.ApprovalID).To(Equal(uint64(42)))
					Expect(dep.PreviewMeta.RunID).To(Equal(int64(12345)))
					Expect(dep.PreviewMeta.Status).To(Equal("succeeded"))
					return nil
				},
			)

			err := s.OnPreviewComplete(context.Background(), 1, depsvcapi.PreviewResult{
				RunID:   12345,
				RunURL:  "https://github.com/org/repo/actions/runs/12345",
				Status:  "succeeded",
				Summary: "No changes detected",
			})
			Expect(err).To(BeNil())
		})

		It("should transition to preview_failed on failure", func() {
			dep := &cp.Deployment{
				ID:    1,
				Phase: cp.PhasePreviewPending,
			}

			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)
			mockRepo.EXPECT().GetAndPatch(gomock.Any(), uint64(1), gomock.Any()).DoAndReturn(
				func(ctx context.Context, id uint64, patches ...repo.PatchDeployment) error {
					for _, p := range patches {
						if err := p(dep); err != nil {
							return err
						}
					}
					Expect(dep.Phase).To(Equal(cp.PhasePreviewFailed))
					return nil
				},
			)

			err := s.OnPreviewComplete(context.Background(), 1, depsvcapi.PreviewResult{
				RunID:  12345,
				Status: "failed",
			})
			Expect(err).To(BeNil())
		})
	})

	Describe("[RetryApply]", func() {
		It("should retry from apply_failed using same frozen snapshot", func() {
			dep := &cp.Deployment{
				ID:       1,
				Scenario: cp.ScenarioAWSPremiumRegion,
				Phase:    cp.PhaseApplyFailed,
				Params:   validAWSPremiumParams,
				FrozenSnapshot: &cp.FrozenSnapshot{
					WorkflowName:   "deploy-aws-premium-region.yml",
					WorkflowInputs: map[string]string{"preview": "true", "region-name": "us-west-2-premium-0", "environment": "dev"},
				},
				ApplyMeta: &cp.WorkflowRunMeta{
					RetryCount: 1,
				},
			}

			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)
			mockGHCli.EXPECT().DeployWorkflow(gomock.Any(), "deploy-aws-premium-region.yml", gomock.Any()).DoAndReturn(
				func(ctx context.Context, wf string, req interface{}) (int64, string, error) {
					// Verify apply uses preview=false
					reqMap := req.(map[string]interface{})
					inputs := reqMap["inputs"].(map[string]string)
					Expect(inputs["preview"]).To(Equal("false"))
					return int64(99002), "https://github.com/tidbcloud/auto-deploy/actions/runs/99002", nil
				},
			)
			mockRepo.EXPECT().GetAndPatch(gomock.Any(), uint64(1), gomock.Any()).DoAndReturn(
				func(ctx context.Context, id uint64, patches ...repo.PatchDeployment) error {
					for _, p := range patches {
						if err := p(dep); err != nil {
							return err
						}
					}
					Expect(dep.Phase).To(Equal(cp.PhaseApplyRunning))
					Expect(dep.ApplyMeta.RetryCount).To(Equal(2))
					return nil
				},
			)

			err := s.RetryApply(context.Background(), 1)
			Expect(err).To(BeNil())
		})

		It("should reject retry from non-apply_failed phase", func() {
			dep := &cp.Deployment{
				ID:    1,
				Phase: cp.PhaseApplyRunning,
			}

			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)

			err := s.RetryApply(context.Background(), 1)
			Expect(err).ToNot(BeNil())
		})
	})

	Describe("[Cancel]", func() {
		It("should cancel a non-terminal deployment", func() {
			dep := &cp.Deployment{
				ID:    1,
				Phase: cp.PhasePreviewRunning,
			}

			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)
			mockRepo.EXPECT().GetAndPatch(gomock.Any(), uint64(1), gomock.Any()).DoAndReturn(
				func(ctx context.Context, id uint64, patches ...repo.PatchDeployment) error {
					for _, p := range patches {
						if err := p(dep); err != nil {
							return err
						}
					}
					Expect(dep.Phase).To(Equal(cp.PhaseCanceled))
					Expect(dep.ClosedAt).ToNot(BeNil())
					return nil
				},
			)

			err := s.Cancel(context.Background(), 1)
			Expect(err).To(BeNil())
		})

		It("should reject canceling a terminal deployment", func() {
			dep := &cp.Deployment{
				ID:    1,
				Phase: cp.PhaseApplySucceeded,
			}

			mockRepo.EXPECT().Get(gomock.Any(), uint64(1)).Return(dep, nil)

			err := s.Cancel(context.Background(), 1)
			Expect(err).ToNot(BeNil())
		})
	})
})
