package clusterrecovery

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

	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
)

func TestGormStoreApplyCreateRequiresExistingOrganizationAndIsIdempotent(t *testing.T) {
	gormDB := recoveryTestDB(t)
	store := NewGormStore(gormDB)
	desired := mutationDesired("1", 2, 10)
	desiredChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, desired)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := store.ApplyCreate(context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum); !errors.Is(err, ErrMutationConflict) {
		t.Fatalf("expected missing-org conflict, got %v", err)
	}
	var orgCount int64
	if err := gormDB.Unscoped().Model(&db.Organization{}).Count(&orgCount).Error; err != nil {
		t.Fatal(err)
	}
	if orgCount != 0 {
		t.Fatalf("guarded create synthesized %d organizations", orgCount)
	}
	createMutationOrg(t, gormDB, 10, 2)
	result, err := store.ApplyCreate(context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum)
	if err != nil {
		t.Fatalf("ApplyCreate() error = %v", err)
	}
	if result.Outcome != MutationOutcomeApplied || !isSHA256(result.AfterChecksum) {
		t.Fatalf("result = %+v", result)
	}
	result, err = store.ApplyCreate(context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum)
	if err != nil {
		t.Fatalf("idempotent ApplyCreate() error = %v", err)
	}
	if result.Outcome != MutationOutcomeNoop || result.BeforeChecksum != result.AfterChecksum {
		t.Fatalf("idempotent result = %+v", result)
	}
}

func TestGormStoreApplyUpdateWritesExplicitZeroValues(t *testing.T) {
	gormDB := recoveryTestDB(t)
	createMutationOrg(t, gormDB, 10, 2)
	current := mutationDesired("1", 2, 10)
	current.StartTime = 100
	current.ProjectID = "123"
	if err := gormDB.Omit("Organization").Create(current).Error; err != nil {
		t.Fatal(err)
	}
	beforeChecksum, err := clusterRecordChecksum(current)
	if err != nil {
		t.Fatal(err)
	}
	desired := *current
	desired.StartTime = 0
	desired.ProjectID = ""
	desiredChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, &desired)
	if err != nil {
		t.Fatal(err)
	}
	fields := []string{"project_id", "start_time"}
	result, err := NewGormStore(gormDB).ApplyUpdate(
		context.Background(), ObjectKindMgmtCluster, &desired, desiredChecksum, beforeChecksum, fields,
	)
	if err != nil {
		t.Fatalf("ApplyUpdate() error = %v", err)
	}
	if result.Outcome != MutationOutcomeApplied || result.BeforeChecksum == result.AfterChecksum {
		t.Fatalf("result = %+v", result)
	}
	var stored db.Cluster
	if err := gormDB.Unscoped().Where("cluster_id = ?", desired.ClusterID).Take(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if stored.StartTime != 0 || stored.ProjectID != "" {
		t.Fatalf("zero values were omitted: start=%d project=%q", stored.StartTime, stored.ProjectID)
	}
}

func TestGormStoreApplyUpdateRejectsChecksumDrift(t *testing.T) {
	gormDB := recoveryTestDB(t)
	createMutationOrg(t, gormDB, 10, 2)
	current := mutationDesired("1", 2, 10)
	current.Status = "modifying"
	if err := gormDB.Omit("Organization").Create(current).Error; err != nil {
		t.Fatal(err)
	}
	desired := *current
	desired.Status = "active"
	desiredChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, &desired)
	if err != nil {
		t.Fatal(err)
	}
	_, err = NewGormStore(gormDB).ApplyUpdate(
		context.Background(), ObjectKindMgmtCluster, &desired, desiredChecksum, stringsOf('f', 64), []string{"status"},
	)
	if !errors.Is(err, ErrRowDrift) {
		t.Fatalf("expected row drift, got %v", err)
	}
}

func TestGormStoreApplyCreateRequiresActiveSameTenantParent(t *testing.T) {
	gormDB := recoveryTestDB(t)
	createMutationOrg(t, gormDB, 10, 2)
	desired := mutationDesired("2", 2, 10)
	desired.ParentID = "1"
	desiredChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, desired)
	if err != nil {
		t.Fatal(err)
	}
	_, err = NewGormStore(gormDB).ApplyCreate(context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum)
	if !errors.Is(err, ErrParentUnavailable) {
		t.Fatalf("expected unavailable parent, got %v", err)
	}
	parent := mutationDesired("1", 99, 10)
	if err := gormDB.Omit("Organization").Create(parent).Error; err != nil {
		t.Fatal(err)
	}
	_, err = NewGormStore(gormDB).ApplyCreate(context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum)
	if !errors.Is(err, ErrParentUnavailable) {
		t.Fatalf("expected cross-tenant parent rejection, got %v", err)
	}
}

func TestGormStoreApplyCreatePersistsDeletedHistory(t *testing.T) {
	gormDB := recoveryTestDB(t)
	createMutationOrg(t, gormDB, 10, 2)
	deletedAt := time.Date(2026, 7, 20, 2, 0, 0, 0, time.UTC)
	desired := terminalDesired(mutationDesired("1", 2, 10), deletedAt)
	desiredChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, desired)
	if err != nil {
		t.Fatal(err)
	}
	result, err := NewGormStore(gormDB).ApplyCreate(
		context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum,
	)
	if err != nil {
		t.Fatalf("ApplyCreate() error = %v", err)
	}
	if result.Outcome != MutationOutcomeApplied {
		t.Fatalf("result = %+v", result)
	}
	var stored db.Cluster
	if err := gormDB.Unscoped().Where("cluster_id = ?", desired.ClusterID).Take(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if stored.Status != "deleted" || !stored.DeletedAt.Valid || stored.DeletedAt.Time.UTC().Unix() != deletedAt.Unix() ||
		stored.EndTime != uint64(deletedAt.Unix()) {
		t.Fatalf("deleted lifecycle was not preserved: %+v", stored)
	}
	result, err = NewGormStore(gormDB).ApplyCreate(
		context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum,
	)
	if err != nil || result.Outcome != MutationOutcomeNoop {
		t.Fatalf("idempotent terminal create result=%+v err=%v", result, err)
	}
}

func TestGormStoreApplyUpdateMarksActiveRowDeleted(t *testing.T) {
	gormDB := recoveryTestDB(t)
	createMutationOrg(t, gormDB, 10, 2)
	current := mutationDesired("1", 2, 10)
	if err := gormDB.Omit("Organization").Create(current).Error; err != nil {
		t.Fatal(err)
	}
	beforeChecksum, err := clusterRecordChecksum(current)
	if err != nil {
		t.Fatal(err)
	}
	desired := terminalDesired(current, time.Date(2026, 7, 20, 2, 0, 0, 0, time.UTC))
	desiredChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, desired)
	if err != nil {
		t.Fatal(err)
	}
	result, err := NewGormStore(gormDB).ApplyUpdate(
		context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum, beforeChecksum,
		[]string{"deleted_at", "end_time", "status"},
	)
	if err != nil {
		t.Fatalf("ApplyUpdate() error = %v", err)
	}
	if result.Outcome != MutationOutcomeApplied {
		t.Fatalf("result = %+v", result)
	}
	var stored db.Cluster
	if err := gormDB.Unscoped().Where("cluster_id = ?", desired.ClusterID).Take(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if stored.Status != "deleted" || !stored.DeletedAt.Valid || stored.EndTime != desired.EndTime {
		t.Fatalf("row was not marked deleted: %+v", stored)
	}
}

func TestGormStoreRejectsResurrectingDeletedHistory(t *testing.T) {
	gormDB := recoveryTestDB(t)
	createMutationOrg(t, gormDB, 10, 2)
	current := terminalDesired(mutationDesired("1", 2, 10), time.Date(2026, 7, 20, 2, 0, 0, 0, time.UTC))
	if err := gormDB.Unscoped().Omit("Organization").Create(current).Error; err != nil {
		t.Fatal(err)
	}
	beforeChecksum, err := clusterRecordChecksum(current)
	if err != nil {
		t.Fatal(err)
	}
	desired := mutationDesired("1", 2, 10)
	desiredChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, desired)
	if err != nil {
		t.Fatal(err)
	}
	_, err = NewGormStore(gormDB).ApplyUpdate(
		context.Background(), ObjectKindMgmtCluster, desired, desiredChecksum, beforeChecksum,
		[]string{"deleted_at", "end_time", "status"},
	)
	if !errors.Is(err, ErrMutationConflict) {
		t.Fatalf("expected resurrection conflict, got %v", err)
	}
}

func TestGormStoreCreatesTenantZeroResourcePoolOnly(t *testing.T) {
	gormDB := recoveryTestDB(t)
	createMutationOrg(t, gormDB, 10, 0)
	desired := mutationDesired("631176026126286850", 0, 10)
	desired.DeployType = "nextgen-host"
	desired.DeployTypeV2 = "essential v2 resource pool"
	desiredChecksum, err := desiredClusterChecksum(ObjectKindResourcePool, desired)
	if err != nil {
		t.Fatal(err)
	}

	result, err := NewGormStore(gormDB).ApplyCreate(
		context.Background(), ObjectKindResourcePool, desired, desiredChecksum,
	)
	if err != nil {
		t.Fatalf("ApplyCreate() shared pool error = %v", err)
	}
	if result.Outcome != MutationOutcomeApplied {
		t.Fatalf("result = %+v", result)
	}
	var stored db.Cluster
	if err := gormDB.Unscoped().Where("cluster_id = ?", desired.ClusterID).Take(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if stored.TenantID != 0 || stored.OrgID != 10 || stored.DeployTypeV2 != desired.DeployTypeV2 {
		t.Fatalf("unexpected shared resource-pool row: %+v", stored)
	}

	mgmt := mutationDesired("10914645658854034992", 0, 10)
	mgmtChecksum, err := desiredClusterChecksum(ObjectKindMgmtCluster, mgmt)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := NewGormStore(gormDB).ApplyCreate(context.Background(), ObjectKindMgmtCluster, mgmt, mgmtChecksum); !errors.Is(err, ErrMutationConflict) {
		t.Fatalf("expected tenant-zero mgmt mutation conflict, got %v", err)
	}
}

func mutationDesired(clusterID string, tenantID uint64, orgID uint) *db.Cluster {
	return &db.Cluster{
		Model:        gorm.Model{CreatedAt: time.Date(2026, 7, 20, 1, 0, 0, 0, time.UTC)},
		ClusterID:    clusterID,
		ClusterType:  "cloud",
		TenantID:     tenantID,
		OrgID:        orgID,
		Name:         "masked-test-cluster",
		Status:       "active",
		Provider:     "aws",
		Region:       "us-west-2",
		DeployType:   "premium",
		DeployTypeV2: "premium",
		Version:      "v8.5.0",
	}
}

func createMutationOrg(t *testing.T, gormDB *gorm.DB, id uint, tenantID uint64) {
	t.Helper()
	org := db.Organization{
		Model:      gorm.Model{ID: id},
		Name:       "masked-org",
		DbaasOrgID: uint(tenantID),
		OrgType:    string(view.CloudDataSource),
	}
	if err := gormDB.Create(&org).Error; err != nil {
		t.Fatal(err)
	}
}

func stringsOf(value byte, count int) string {
	bytes := make([]byte, count)
	for i := range bytes {
		bytes[i] = value
	}
	return string(bytes)
}
