package clusterrecovery

import (
	"context"
	"errors"
	"testing"
	"time"

	commonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	serverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/cloudevent"
	"github.com/pingcap-inc/clinic/pkg/nextgen_client"
)

func TestCandidateFromSeedUsesExistingEventTaxonomy(t *testing.T) {
	now := time.Now().UTC()
	tests := []struct {
		name        string
		clusterType string
		evidence    EvidenceType
		want        ObjectKind
	}{
		{name: "legacy mgmt", clusterType: "", evidence: EvidenceClusterEventDLQ, want: ObjectKindMgmtCluster},
		{name: "dedicated", clusterType: cloudevent.ClusterTypeDedicatedCluster, evidence: EvidenceClusterEventDLQ, want: ObjectKindMgmtCluster},
		{name: "instance", clusterType: cloudevent.ClusterTypeTidbxInstance, evidence: EvidenceClusterEventDLQ, want: ObjectKindMgmtCluster},
		{name: "pool", clusterType: cloudevent.ClusterTypeTidbxResourcePool, evidence: EvidenceClusterEventDLQ, want: ObjectKindResourcePool},
		{name: "import", clusterType: "", evidence: EvidenceImportDLQ, want: ObjectKindMgmtCluster},
		{name: "unsupported", clusterType: "future_type", evidence: EvidenceClusterEventDLQ, want: ObjectKindUnsupported},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			candidate := CandidateFromSeed(SeedCandidate{
				ClusterID:      "1",
				TenantID:       2,
				ClusterType:    tt.clusterType,
				Evidence:       tt.evidence,
				EventCreatedAt: now,
			})
			if candidate.Kind != tt.want {
				t.Fatalf("kind = %s, want %s", candidate.Kind, tt.want)
			}
		})
	}
}

func TestCloudSourceResolvesMgmtAndDerivesParent(t *testing.T) {
	now := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
	cluster := testNextGenMgmtCluster(now, false)
	source := &CloudSource{
		getMgmtClusters: func(_ *session.CtxUser, ids []uint64) ([]*mgmtv1.Cluster, error) {
			if len(ids) != 1 || ids[0] != cluster.ClusterId {
				t.Fatalf("unexpected ids: %v", ids)
			}
			return []*mgmtv1.Cluster{cluster}, nil
		},
	}
	candidate := Candidate{
		Kind:        ObjectKindMgmtCluster,
		ClusterID:   "10914645658854034992",
		TenantID:    10392,
		ClusterType: cloudevent.ClusterTypeTidbxInstance,
		Evidence:    []Evidence{{Type: EvidenceClusterEventDLQ, ObservedAt: now}},
	}
	resolved, err := source.Resolve(context.Background(), candidate)
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if resolved.Terminal || resolved.Desired == nil {
		t.Fatalf("unexpected resolved source: %+v", resolved)
	}
	if resolved.Parent == nil || resolved.Parent.ClusterID != "631161640854749185" {
		t.Fatalf("parent = %+v", resolved.Parent)
	}
	if resolved.Desired.TenantID != candidate.TenantID {
		t.Fatalf("desired tenant = %d", resolved.Desired.TenantID)
	}
}

func TestCloudSourceConvertsDeletedMgmtForHistoricalRecovery(t *testing.T) {
	now := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
	cluster := testNextGenMgmtCluster(now, true)
	source := &CloudSource{
		getMgmtClusters: func(*session.CtxUser, []uint64) ([]*mgmtv1.Cluster, error) {
			return []*mgmtv1.Cluster{cluster}, nil
		},
	}
	resolved, err := source.Resolve(context.Background(), Candidate{
		Kind:      ObjectKindMgmtCluster,
		ClusterID: "10914645658854034992",
		TenantID:  10392,
	})
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if !resolved.Terminal || resolved.Desired == nil || resolved.Desired.Status != "deleted" ||
		!resolved.Desired.DeletedAt.Valid || resolved.Desired.EndTime == 0 || resolved.Parent != nil {
		t.Fatalf("expected terminal source, got %+v", resolved)
	}
}

func TestCloudSourceDistinguishesNotFoundAndOwnership(t *testing.T) {
	now := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
	candidate := Candidate{Kind: ObjectKindMgmtCluster, ClusterID: "10914645658854034992", TenantID: 10392}
	source := &CloudSource{getMgmtClusters: func(*session.CtxUser, []uint64) ([]*mgmtv1.Cluster, error) {
		return nil, nil
	}}
	if _, err := source.Resolve(context.Background(), candidate); !errors.Is(err, ErrSourceNotFound) {
		t.Fatalf("expected not found, got %v", err)
	}
	source.getMgmtClusters = func(*session.CtxUser, []uint64) ([]*mgmtv1.Cluster, error) {
		cluster := testNextGenMgmtCluster(now, false)
		cluster.OrganizationId++
		return []*mgmtv1.Cluster{cluster}, nil
	}
	if _, err := source.Resolve(context.Background(), candidate); !errors.Is(err, ErrSourceOwnership) {
		t.Fatalf("expected ownership error, got %v", err)
	}
}

func TestValidateMgmtTypeUsesCurrentAuthoritativeDeployType(t *testing.T) {
	cluster := &mgmtv1.Cluster{
		DeployType: mgmtv1.DeployType_DEPLOY_TYPE_SERVERLESS,
		ClusterInfo: &mgmtv1.Cluster_ServerlessCluster{
			ServerlessCluster: &serverlessv1.ServerlessServiceCluster{},
		},
	}
	for _, eventType := range []string{
		cloudevent.ClusterTypeTidbxInstance,
		cloudevent.ClusterTypeDedicatedCluster,
		"",
	} {
		candidate := Candidate{Kind: ObjectKindMgmtCluster, ClusterType: eventType}
		if err := validateMgmtType(candidate, cluster); err != nil {
			t.Fatalf("event type %q rejected current serverless source: %v", eventType, err)
		}
	}

	cluster.ClusterInfo = nil
	if err := validateMgmtType(Candidate{Kind: ObjectKindMgmtCluster}, cluster); !errors.Is(err, ErrSourceIncomplete) {
		t.Fatalf("expected incomplete source payload, got %v", err)
	}
}

func TestCloudSourceResolvesOrganizationResourcePool(t *testing.T) {
	now := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
	profile := testResourcePoolProfile(now, false)
	source := &CloudSource{
		listPools: func(*session.CtxUser, []uint64) ([]*nextgenv1.ResourcePoolProfile, error) {
			return []*nextgenv1.ResourcePoolProfile{profile}, nil
		},
		getPool: func(*session.CtxUser, uint64) (*nextgenv1.ResourcePoolProfile, *nextgen_client.SharedPool, error) {
			return profile, &nextgen_client.SharedPool{Phase: "AVAILABLE"}, nil
		},
	}
	resolved, err := source.Resolve(context.Background(), Candidate{
		Kind:        ObjectKindResourcePool,
		ClusterID:   "631161640854749185",
		TenantID:    10392,
		ClusterType: cloudevent.ClusterTypeTidbxResourcePool,
	})
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if resolved.Terminal || resolved.Desired == nil || resolved.Desired.Status != "active" {
		t.Fatalf("unexpected resolved pool: %+v", resolved)
	}
}

func TestCloudSourceResolvesSharedResourcePoolForTenantZero(t *testing.T) {
	now := time.Date(2026, 8, 3, 7, 19, 25, 0, time.UTC)
	profile := testResourcePoolProfile(now, false)
	profile.ResourcePool.OwnerType = nextgenv1.ResourcePool_SHARED
	profile.ResourcePool.OwnerId = 0
	profile.ResourcePool.ServicePlan = commonv1.ServicePlan_Essential_V2
	source := &CloudSource{
		listPools: func(*session.CtxUser, []uint64) ([]*nextgenv1.ResourcePoolProfile, error) {
			return []*nextgenv1.ResourcePoolProfile{profile}, nil
		},
		getPool: func(*session.CtxUser, uint64) (*nextgenv1.ResourcePoolProfile, *nextgen_client.SharedPool, error) {
			return profile, &nextgen_client.SharedPool{Phase: "AVAILABLE"}, nil
		},
	}
	resolved, err := source.Resolve(context.Background(), Candidate{
		Kind:        ObjectKindResourcePool,
		ClusterID:   "631161640854749185",
		TenantID:    0,
		ClusterType: cloudevent.ClusterTypeTidbxResourcePool,
	})
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if resolved.Desired == nil || resolved.Desired.TenantID != 0 ||
		resolved.Desired.DeployTypeV2 != "essential v2 resource pool" || resolved.Desired.Status != "active" {
		t.Fatalf("unexpected shared resource pool: %+v", resolved)
	}
}

func TestCloudSourceRejectsResourcePoolOwnershipMismatch(t *testing.T) {
	now := time.Date(2026, 8, 3, 7, 19, 25, 0, time.UTC)
	tests := []struct {
		name      string
		ownerType nextgenv1.ResourcePool_OwnerType
		ownerID   uint64
		tenantID  uint64
	}{
		{name: "shared pool under organization tenant", ownerType: nextgenv1.ResourcePool_SHARED, ownerID: 0, tenantID: 10392},
		{name: "organization pool under system tenant", ownerType: nextgenv1.ResourcePool_ORGANIZATION, ownerID: 10392, tenantID: 0},
		{name: "shared pool with nonzero owner", ownerType: nextgenv1.ResourcePool_SHARED, ownerID: 10392, tenantID: 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			profile := testResourcePoolProfile(now, false)
			profile.ResourcePool.OwnerType = tt.ownerType
			profile.ResourcePool.OwnerId = tt.ownerID
			source := &CloudSource{
				listPools: func(*session.CtxUser, []uint64) ([]*nextgenv1.ResourcePoolProfile, error) {
					return []*nextgenv1.ResourcePoolProfile{profile}, nil
				},
				getPool: func(*session.CtxUser, uint64) (*nextgenv1.ResourcePoolProfile, *nextgen_client.SharedPool, error) {
					return profile, &nextgen_client.SharedPool{Phase: "AVAILABLE"}, nil
				},
			}
			_, err := source.Resolve(context.Background(), Candidate{
				Kind:      ObjectKindResourcePool,
				ClusterID: "631161640854749185",
				TenantID:  tt.tenantID,
			})
			if !errors.Is(err, ErrSourceOwnership) {
				t.Fatalf("expected ownership rejection, got %v", err)
			}
		})
	}
}

func TestCloudSourceRejectsIncompleteActiveResourcePool(t *testing.T) {
	now := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
	profile := testResourcePoolProfile(now, false)
	source := &CloudSource{
		listPools: func(*session.CtxUser, []uint64) ([]*nextgenv1.ResourcePoolProfile, error) {
			return []*nextgenv1.ResourcePoolProfile{profile}, nil
		},
		getPool: func(*session.CtxUser, uint64) (*nextgenv1.ResourcePoolProfile, *nextgen_client.SharedPool, error) {
			return profile, nil, nil
		},
	}
	_, err := source.Resolve(context.Background(), Candidate{
		Kind:      ObjectKindResourcePool,
		ClusterID: "631161640854749185",
		TenantID:  10392,
	})
	if !errors.Is(err, ErrSourceIncomplete) {
		t.Fatalf("expected incomplete source, got %v", err)
	}
}

func TestCloudSourceConvertsDeletedResourcePoolWithoutSharedPool(t *testing.T) {
	now := time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)
	profile := testResourcePoolProfile(now, true)
	source := &CloudSource{
		listPools: func(*session.CtxUser, []uint64) ([]*nextgenv1.ResourcePoolProfile, error) {
			return []*nextgenv1.ResourcePoolProfile{profile}, nil
		},
		getPool: func(*session.CtxUser, uint64) (*nextgenv1.ResourcePoolProfile, *nextgen_client.SharedPool, error) {
			return profile, nil, nil
		},
	}
	resolved, err := source.Resolve(context.Background(), Candidate{
		Kind:      ObjectKindResourcePool,
		ClusterID: "631161640854749185",
		TenantID:  10392,
	})
	if err != nil {
		t.Fatalf("Resolve() error = %v", err)
	}
	if !resolved.Terminal || resolved.Desired == nil || resolved.Desired.Status != "deleted" ||
		!resolved.Desired.DeletedAt.Valid || resolved.Desired.EndTime == 0 {
		t.Fatalf("unexpected deleted resource pool: %+v", resolved)
	}
}

func testNextGenMgmtCluster(now time.Time, deleted bool) *mgmtv1.Cluster {
	cluster := &mgmtv1.Cluster{
		ClusterId:      10914645658854034992,
		OrganizationId: 10392,
		ClusterName:    "masked-test-cluster",
		CloudProvider:  "aws",
		Region:         "us-west-2",
		DeployType:     mgmtv1.DeployType_DEPLOY_TYPE_NEXTGEN,
		CreateTime:     timestamppb.New(now.Add(-time.Hour)),
		UpdateTime:     timestamppb.New(now),
		ClusterInfo: &mgmtv1.Cluster_NextGenCluster{NextGenCluster: &nextgenv1.Cluster{
			ClusterId:      10914645658854034992,
			OrganizationId: 10392,
			State:          nextgenv1.Cluster_AVAILABLE,
			ServicePlan:    commonv1.ServicePlan_Premium,
			SharedPoolId:   "631161640854749185",
			Version:        "v8.5.0",
		}},
	}
	if deleted {
		cluster.DeleteTime = timestamppb.New(now.Add(time.Minute))
		cluster.GetNextGenCluster().State = nextgenv1.Cluster_DELETED
	}
	return cluster
}

func testResourcePoolProfile(now time.Time, deleted bool) *nextgenv1.ResourcePoolProfile {
	pool := &nextgenv1.ResourcePool{
		Id:          631161640854749185,
		Name:        "masked-test-pool",
		OwnerType:   nextgenv1.ResourcePool_ORGANIZATION,
		OwnerId:     10392,
		ServicePlan: commonv1.ServicePlan_Premium,
		CreateTime:  timestamppb.New(now.Add(-time.Hour)),
		UpdateTime:  timestamppb.New(now),
	}
	if deleted {
		pool.DeleteTime = timestamppb.New(now.Add(time.Minute))
	}
	return &nextgenv1.ResourcePoolProfile{
		ResourcePool: pool,
		Version:      "v8.5.0",
		Region:       &commonv1.Region{State: commonv1.Region_ACTIVE, LogicalName: "us-west-2"},
	}
}
