package operation

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

	maintent "github.com/tidbcloud/devops/entity/maintenance"
	opent "github.com/tidbcloud/devops/entity/operation"
	tntent "github.com/tidbcloud/devops/entity/tenant"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	testutil "github.com/tidbcloud/devops/pkg/infra/utils/test"
	"github.com/tidbcloud/devops/pkg/service"
	apprsvcapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	actregistry "github.com/tidbcloud/devops/pkg/service/operation/actions/registry"
	opregistry "github.com/tidbcloud/devops/pkg/service/operation/operation"
	"github.com/tidbcloud/devops/pkg/service/operation/repo"
	tenantsvc "github.com/tidbcloud/devops/pkg/service/tenant"
	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"go.uber.org/mock/gomock"
	"google.golang.org/grpc"
)

func TestCreateInstanceFillsOperatorEntity(t *testing.T) {
	tests := []struct {
		name  string
		ctx   context.Context
		param CreateInstanceParam
		want  string
	}{
		{
			name: "uses operator from context",
			ctx: context.WithValue(
				context.WithValue(context.Background(), consts.CtxOperatorType, consts.CtxOperatorTypeAPIKey),
				consts.CtxOperator,
				"key-a",
			),
			param: CreateInstanceParam{Applicant: "owner@pingcap.com"},
			want:  "api_key:key-a",
		},
		{
			name:  "falls back to applicant without operator context",
			ctx:   context.Background(),
			param: CreateInstanceParam{Applicant: "owner@pingcap.com"},
			want:  "unknown:owner@pingcap.com",
		},
		{
			name: "preserves prefilled operator",
			ctx: context.WithValue(
				context.WithValue(context.Background(), consts.CtxOperatorType, consts.CtxOperatorTypeAPIKey),
				consts.CtxOperator,
				"key-a",
			),
			param: CreateInstanceParam{
				Applicant:      "owner@pingcap.com",
				OperatorEntity: "user:prefilled@pingcap.com",
			},
			want: "user:prefilled@pingcap.com",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			apprSvc := apprsvcapi.NewMockApprovalServiceAPI(ctrl)
			apprSvc.EXPECT().
				RegisterTemplate(gomock.Any(), gomock.Any()).
				Return(nil)

			operationFile := filepath.Join(t.TempDir(), "operations.yaml")
			if err := os.WriteFile(operationFile, []byte(`
- name: test-operation
  alias: Test Operation
  target: cluster
`), 0o600); err != nil {
				t.Fatalf("write operation registry file: %v", err)
			}
			opRegistry, err := opregistry.InitRegistry(operationFile, apprSvc)
			if err != nil {
				t.Fatalf("init operation registry: %v", err)
			}

			dbp := testutil.NewMockTransProvider(ctrl)
			dbp.EXPECT().
				Transaction(gomock.Any(), gomock.Any()).
				DoAndReturn(func(ctx context.Context, cb func(context.Context) error) error {
					return cb(ctx)
				})

			var gotOperatorEntity string
			opRepo := repo.NewMockOperationRepo(ctrl)
			opRepo.EXPECT().
				CreateInstance(gomock.Any(), gomock.Any()).
				DoAndReturn(func(_ context.Context, ins *opent.Instance) (*opent.Instance, error) {
					gotOperatorEntity = ins.OperatorEntity
					ins.ID = 1
					return ins, nil
				})

			s := &svc{
				JobService:  service.NewJobService("OperationService"),
				dbp:         dbp,
				eventbus:    eventbus.NewMockEventBus(),
				opRepo:      opRepo,
				opRegistry:  opRegistry,
				actRegistry: actregistry.Registry{},
				syncInsCh:   make(chan uint64, 1),
			}
			tc.param.Type = opent.OperationTargetCluster
			tc.param.Operation = "test-operation"
			tc.param.Applicant = "owner@pingcap.com"

			if _, err := s.CreateInstance(tc.ctx, tc.param); err != nil {
				t.Fatalf("CreateInstance returned error: %v", err)
			}
			if gotOperatorEntity != tc.want {
				t.Fatalf("operator entity = %q, want %q", gotOperatorEntity, tc.want)
			}
		})
	}
}

func TestInjectParamForResourcePoolTarget(t *testing.T) {
	tests := []struct {
		name               string
		opName             string
		resourcePool       *nextgenv1.ResourcePool
		expectTenantLookup bool
		wantTenantID       string
		wantTenantRegion   string
		wantTenantConst    bool
		wantTenantDefault  string
	}{
		{
			name:   "shared resource pool uses tenant zero",
			opName: "DeleteResourcePool",
			resourcePool: &nextgenv1.ResourcePool{
				Id:        123,
				OwnerType: nextgenv1.ResourcePool_SHARED,
			},
			wantTenantID:     "0",
			wantTenantRegion: opent.ParamTenantRegionSharedPool,
			wantTenantConst:  true,
		},
		{
			name:   "shared resource pool set default allows tenant override",
			opName: "SetDefaultResourcePool",
			resourcePool: &nextgenv1.ResourcePool{
				Id:        123,
				OwnerType: nextgenv1.ResourcePool_SHARED,
			},
			wantTenantRegion:  opent.ParamTenantRegionSharedPool,
			wantTenantConst:   false,
			wantTenantDefault: "0",
		},
		{
			name:   "organization resource pool keeps owner tenant injection",
			opName: "DeleteResourcePool",
			resourcePool: &nextgenv1.ResourcePool{
				Id:        123,
				OwnerType: nextgenv1.ResourcePool_ORGANIZATION,
				OwnerId:   456,
			},
			expectTenantLookup: true,
			wantTenantID:       "456",
			wantTenantRegion:   string(tntent.TenantRegionAPAC),
			wantTenantConst:    true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			tenantSvc := tenantsvc.NewMockTenantServiceAPI(ctrl)
			if tc.expectTenantLookup {
				tenantSvc.EXPECT().
					GetTenant(gomock.Any(), uint64(456)).
					Return(&tntent.Tenant{CountryRegion: tntent.TenantRegionAPAC}, nil)
			}

			s := &svc{
				coresvcCli: &coresvccli.CoreSvcCli{
					Mgmt: &coresvccli.MgmtService{
						ResourcePool: fakeResourcePoolServiceClient{
							resourcePool: tc.resourcePool,
						},
					},
				},
				tenantSvc: tenantSvc,
			}
			op := &opent.Operation{
				Name: tc.opName,
				Params: map[opent.ParamName]opent.ParamDefine{
					opent.ParamResourcePoolID: {},
					opent.ParamTenantID:       {},
					opent.ParamTenantRegion:   {},
					opent.ParamProvider:       {},
					opent.ParamRegion:         {},
				},
			}

			err := s.injectParam(context.Background(), op, GetOperationParam{
				InjectParam: true,
				TargetType:  pointer.Pointer(opent.OperationTargetResourcePool),
				TargetID:    pointer.Pointer("123"),
			})
			if err != nil {
				t.Fatalf("injectParam returned error: %v", err)
			}

			assertConstParam(t, op, opent.ParamResourcePoolID, "123")
			if tc.wantTenantConst {
				assertConstParam(t, op, opent.ParamTenantID, tc.wantTenantID)
			} else {
				assertDefaultParam(t, op, opent.ParamTenantID, tc.wantTenantDefault)
			}
			assertConstParam(t, op, opent.ParamTenantRegion, tc.wantTenantRegion)
		})
	}
}

func TestListInstancesFillsExecutionWindows(t *testing.T) {
	ctrl := gomock.NewController(t)
	maintSvc := cmxapi.NewMockServiceAPI(ctrl)
	window20 := &maintent.OpsExecutionWindow{ID: 20, StartAt: 100, EndAt: 200}
	window30 := &maintent.OpsExecutionWindow{ID: 30, StartAt: 300, EndAt: 400}
	maintSvc.EXPECT().
		ListOpsExecutionWindows(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, param cmxapi.ListOpsExecutionWindowParam) ([]*maintent.OpsExecutionWindow, int64, error) {
			assertUintSet(t, param.IDs, map[uint64]bool{20: true, 30: true})
			return []*maintent.OpsExecutionWindow{window20, window30}, int64(2), nil
		})

	instances := []*opent.Instance{
		{ID: 1},
		{ID: 2, ExecutionWindowID: pointer.Pointer(uint64(20))},
		{ID: 3, ExecutionWindowID: pointer.Pointer(uint64(30))},
	}
	opRepo := repo.NewMockOperationRepo(ctrl)
	opRepo.EXPECT().
		CountInstance(gomock.Any()).
		Return(int64(len(instances)), nil)
	opRepo.EXPECT().
		ListInstances(gomock.Any()).
		Return(instances, nil)
	s := &svc{
		opRepo:   opRepo,
		maintSvc: maintSvc,
	}

	got, total, err := s.ListInstances(context.Background(), ListInstanceParam{})
	if err != nil {
		t.Fatalf("ListInstances returned error: %v", err)
	}
	if total != int64(len(instances)) {
		t.Fatalf("total = %d, want %d", total, len(instances))
	}
	if got[0].ExecutionWindow != nil {
		t.Fatalf("instance without execution window id got window %#v", got[0].ExecutionWindow)
	}
	if got[1].ExecutionWindow != window20 {
		t.Fatalf("instance 2 execution window = %#v, want %#v", got[1].ExecutionWindow, window20)
	}
	if got[2].ExecutionWindow != window30 {
		t.Fatalf("instance 3 execution window = %#v, want %#v", got[2].ExecutionWindow, window30)
	}
}

func TestGetInstanceFillsExecutionWindow(t *testing.T) {
	ctrl := gomock.NewController(t)
	maintSvc := cmxapi.NewMockServiceAPI(ctrl)
	window := &maintent.OpsExecutionWindow{ID: 20, StartAt: 100, EndAt: 200}
	maintSvc.EXPECT().
		ListOpsExecutionWindows(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, param cmxapi.ListOpsExecutionWindowParam) ([]*maintent.OpsExecutionWindow, int64, error) {
			assertUintSet(t, param.IDs, map[uint64]bool{20: true})
			return []*maintent.OpsExecutionWindow{window}, int64(1), nil
		})
	opRepo := repo.NewMockOperationRepo(ctrl)
	opRepo.EXPECT().
		GetInstance(gomock.Any(), uint64(1)).
		Return(&opent.Instance{ID: 1, ExecutionWindowID: pointer.Pointer(uint64(20))}, nil)

	s := &svc{
		opRepo:   opRepo,
		maintSvc: maintSvc,
	}

	got, err := s.GetInstance(context.Background(), 1)
	if err != nil {
		t.Fatalf("GetInstance returned error: %v", err)
	}
	if got.ExecutionWindow != window {
		t.Fatalf("execution window = %#v, want %#v", got.ExecutionWindow, window)
	}
}

func assertUintSet(t *testing.T, got []uint64, want map[uint64]bool) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("ids = %v, want set %v", got, want)
	}
	for _, id := range got {
		if !want[id] {
			t.Fatalf("unexpected id %d in %v", id, got)
		}
	}
}

func assertConstParam(t *testing.T, op *opent.Operation, name opent.ParamName, want string) {
	t.Helper()

	got, ok := op.Params[name]
	if !ok {
		t.Fatalf("param %s is missing", name)
	}
	if got.Const == nil {
		t.Fatalf("param %s const is nil, want %q", name, want)
	}
	if *got.Const != want {
		t.Fatalf("param %s const = %q, want %q", name, *got.Const, want)
	}
}

func assertDefaultParam(t *testing.T, op *opent.Operation, name opent.ParamName, want string) {
	t.Helper()

	got, ok := op.Params[name]
	if !ok {
		t.Fatalf("param %s is missing", name)
	}
	if got.Const != nil {
		t.Fatalf("param %s const = %q, want nil", name, *got.Const)
	}
	if got.Default == nil {
		t.Fatalf("param %s default is nil, want %q", name, want)
	}
	if *got.Default != want {
		t.Fatalf("param %s default = %q, want %q", name, *got.Default, want)
	}
}

type fakeResourcePoolServiceClient struct {
	mgmtv1.ResourcePoolServiceClient
	resourcePool *nextgenv1.ResourcePool
}

func (f fakeResourcePoolServiceClient) CreateResourcePool(context.Context, *nextgenv1.CreateResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePoolProfile, error) {
	panic("not implemented")
}

func (f fakeResourcePoolServiceClient) ListResourcePools(context.Context, *nextgenv1.ListResourcePoolsRequest, ...grpc.CallOption) (*nextgenv1.ListResourcePoolsResponse, error) {
	panic("not implemented")
}

func (f fakeResourcePoolServiceClient) UpdateResourcePoolName(context.Context, *nextgenv1.UpdateResourcePoolNameRequest, ...grpc.CallOption) (*nextgenv1.ResourcePool, error) {
	panic("not implemented")
}

func (f fakeResourcePoolServiceClient) UpdateResourcePoolInstanceLimit(context.Context, *nextgenv1.UpdateResourcePoolInstanceLimitRequest, ...grpc.CallOption) (*nextgenv1.ResourcePool, error) {
	panic("not implemented")
}

func (f fakeResourcePoolServiceClient) GetResourcePool(context.Context, *nextgenv1.GetResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePoolProfile, error) {
	return &nextgenv1.ResourcePoolProfile{
		ResourcePool: f.resourcePool,
		Region: &commonv1.Region{
			Id:              commonv1.Region_AWS_US_WEST_2,
			CloudProviderId: commonv1.CloudProvider_AWS,
		},
	}, nil
}

func (f fakeResourcePoolServiceClient) DeleteResourcePool(context.Context, *nextgenv1.DeleteResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePool, error) {
	panic("not implemented")
}

func (f fakeResourcePoolServiceClient) ListOrgResourcePools(context.Context, *mgmtv1.ListOrgResourcePoolsRequest, ...grpc.CallOption) (*nextgenv1.ListOwnerResourcePoolsResponse, error) {
	panic("not implemented")
}

func (f fakeResourcePoolServiceClient) UpdateOrgDefaultResourcePool(context.Context, *mgmtv1.UpdateOrgDefaultResourcePoolRequest, ...grpc.CallOption) (*nextgenv1.ResourcePoolStrategy, error) {
	panic("not implemented")
}
