package controlplane

import (
	"database/sql"
	"testing"

	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

func TestValidateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFactsAcceptsStateHashOracles(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("crm", 7)
	tests := []struct {
		name        string
		status      string
		kind        string
		revision    string
		applied     func(*packs.SourceBackedPack) string
		desired     func(*packs.SourceBackedPack) string
		deleted     bool
		wantPending ApplyAllSourceRevisionAdvancePendingKind
	}{
		{name: "active from v1 without desired", status: installedPackStatusActive, kind: advance.Kind, revision: advance.FromRevision, applied: testCurrentTargetV1Hash, wantPending: ApplyAllSourceRevisionAdvancePendingAdvance},
		{name: "active from legacy with stale desired", status: installedPackStatusActive, kind: advance.Kind, revision: advance.FromRevision, applied: testCurrentTargetLegacyHash, desired: func(*packs.SourceBackedPack) string { return "stale-desired-hash" }, wantPending: ApplyAllSourceRevisionAdvancePendingAdvance},
		{name: "failed target without applied", status: installedPackStatusFailed, kind: advance.Kind, revision: advance.ToRevision, desired: testCurrentTargetV1Hash, wantPending: ApplyAllSourceRevisionAdvancePendingRetry},
		{name: "soft-deleted failed target without applied", status: installedPackStatusFailed, kind: advance.Kind, revision: advance.ToRevision, desired: testCurrentTargetV1Hash, deleted: true, wantPending: ApplyAllSourceRevisionAdvancePendingRetry},
		{name: "soft-deleted installing target with stale applied", status: installedPackStatusInstalling, kind: advance.Kind, revision: advance.ToRevision, applied: func(*packs.SourceBackedPack) string { return "stale-from-hash" }, desired: testCurrentTargetV1Hash, deleted: true, wantPending: ApplyAllSourceRevisionAdvancePendingRetry},
		{name: "canonicalize target alias", status: installedPackStatusActive, kind: "in_repo", revision: advance.ToRevision, applied: testCurrentTargetLegacyHash, desired: testCurrentTargetLegacyHash, wantPending: ApplyAllSourceRevisionAdvancePendingCanonicalize},
		{name: "already current", status: installedPackStatusActive, kind: advance.Kind, revision: advance.ToRevision, applied: testCurrentTargetV1Hash, desired: testCurrentTargetV1Hash},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			target := testApplyAllSourceRevisionTarget(t, advance.Name, "1.0.0", advance.Kind, advance.URI, test.revision)
			proof := testApplyAllSourceRevisionAdvanceFrozenCurrentTargetProof(
				t, advance, target, test.status, test.kind, test.applied, test.desired,
			)
			if test.deleted {
				proof.snapshot.entries[0].row.IsDeleted = 1
			}
			pending := proof.snapshot.entries[0].pending
			if (pending == nil) != (test.wantPending == "") || (pending != nil && pending.Kind != test.wantPending) {
				t.Fatalf("pending = %#v, want %q", pending, test.wantPending)
			}
			if err := validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(proof); err != nil {
				t.Fatalf("validate frozen current target facts: %v", err)
			}
		})
	}
	legacyHostColumnTarget := testApplyAllSourceRevisionTarget(
		t, advance.Name, "1.0.0", advance.Kind, advance.URI, advance.FromRevision,
	)
	legacyHostColumn := testApplyAllSourceRevisionAdvanceFrozenCurrentTargetProof(
		t, advance, legacyHostColumnTarget, installedPackStatusActive, advance.Kind,
		testCurrentTargetV1Hash, testCurrentTargetV1Hash,
	)
	legacyHostColumn.snapshot.entries[0].row.HostRequirementsDigest = sql.NullString{}
	if err := validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(legacyHostColumn); err != nil {
		t.Fatalf("validate missing legacy host digest for empty requirements: %v", err)
	}

	empty := applyAllSourceRevisionAdvancePreconditionProof{
		snapshot: applyAllSourceRevisionAdvanceCurrentSnapshot{entries: []applyAllSourceRevisionAdvanceCurrentSnapshotEntry{}},
		current: applyAllSourceRevisionAdvanceTuple{
			plan: []normalizedApplyAllSourceRevisionAdvance{}, targets: map[string]*packs.SourceBackedPack{},
		},
	}
	if err := validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(empty); err != nil {
		t.Fatalf("validate empty frozen current target facts: %v", err)
	}
	assertApplyAllSourceRevisionAdvancePreconditionError(
		t, validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(applyAllSourceRevisionAdvancePreconditionProof{}),
		ApplyAllSourceRevisionAdvancePlanState, -1, "",
	)
}

func TestValidateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFactsRejectsDrift(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("crm", 11)
	target := testApplyAllSourceRevisionTarget(t, advance.Name, "1.0.0", advance.Kind, advance.URI, advance.FromRevision)
	base := testApplyAllSourceRevisionAdvanceFrozenCurrentTargetProof(
		t, advance, target, installedPackStatusActive, advance.Kind, testCurrentTargetV1Hash, testCurrentTargetV1Hash,
	)
	tests := []struct {
		name string
		edit func(*applyAllSourceRevisionAdvanceCurrentSnapshotEntry)
	}{
		{name: "deleted", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) { entry.row.IsDeleted = 1 }},
		{name: "row name", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) { entry.row.Name = "other" }},
		{name: "category", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.row.Category = string(packs.ConnectorPack)
		}},
		{name: "source URI", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) { entry.row.SourceURI.String = "other" }},
		{name: "source revision", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.row.SourceRevision.String = "other"
		}},
		{name: "empty applied hash", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.row.AppliedPayloadHash = sql.NullString{}
		}},
		{name: "wrong applied hash", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.row.AppliedPayloadHash.String = "wrong"
		}},
		{name: "pack version", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) { entry.row.PackVersion.String = "2.0.0" }},
		{name: "host digest", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.row.HostRequirementsDigest.String = "wrong"
		}},
		{name: "pending", edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.pending.Kind = ApplyAllSourceRevisionAdvancePendingRetry
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			candidate := base
			candidate.snapshot.entries = append([]applyAllSourceRevisionAdvanceCurrentSnapshotEntry{}, base.snapshot.entries...)
			if candidate.snapshot.entries[0].pending != nil {
				pending := *candidate.snapshot.entries[0].pending
				candidate.snapshot.entries[0].pending = &pending
			}
			test.edit(&candidate.snapshot.entries[0])
			err := validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(candidate)
			assertApplyAllSourceRevisionAdvancePreconditionError(t, err, ApplyAllSourceRevisionAdvancePlanState, advance.index, advance.Name)
		})
	}

	retryTarget := testApplyAllSourceRevisionTarget(t, advance.Name, "1.0.0", advance.Kind, advance.URI, advance.ToRevision)
	retry := testApplyAllSourceRevisionAdvanceFrozenCurrentTargetProof(
		t, advance, retryTarget, installedPackStatusFailed, advance.Kind, nil, testCurrentTargetV1Hash,
	)
	for _, desired := range []sql.NullString{{}, {Valid: true, String: "wrong"}} {
		candidate := retry
		candidate.snapshot.entries = append([]applyAllSourceRevisionAdvanceCurrentSnapshotEntry{}, retry.snapshot.entries...)
		candidate.snapshot.entries[0].row.DesiredPayloadHash = desired
		assertApplyAllSourceRevisionAdvancePreconditionError(
			t, validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(candidate),
			ApplyAllSourceRevisionAdvancePlanState, advance.index, advance.Name,
		)
	}
	legacyDesired, err := packs.LegacyPackPayloadHash(retryTarget)
	if err != nil {
		t.Fatalf("legacy retry desired hash: %v", err)
	}
	retry.snapshot.entries[0].row.DesiredPayloadHash = sql.NullString{Valid: true, String: legacyDesired}
	assertApplyAllSourceRevisionAdvancePreconditionError(
		t, validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(retry),
		ApplyAllSourceRevisionAdvancePlanState, advance.index, advance.Name,
	)

	duplicate := base
	duplicate.snapshot.entries = append(duplicate.snapshot.entries, duplicate.snapshot.entries[0])
	duplicate.current.plan = append(duplicate.current.plan, advance)
	duplicate.current.targets = map[string]*packs.SourceBackedPack{"crm": target, "extra": target}
	assertApplyAllSourceRevisionAdvancePreconditionError(
		t, validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(duplicate),
		ApplyAllSourceRevisionAdvancePlanState, advance.index, advance.Name,
	)
}

func TestValidateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFactsBindsHostRequirements(t *testing.T) {
	advance := testApplyAllSourceRevisionAdvance("crm", 13)
	target := testApplyAllSourceRevisionTargetWithHostRequirement(t, advance.Name, packs.HostPrimitiveRequirement{
		Kind: packs.HostPrimitiveKindProductDialect, Name: "sf-crm", Version: "1",
	})
	proof := testApplyAllSourceRevisionAdvanceFrozenCurrentTargetProof(
		t, advance, target, installedPackStatusActive, advance.Kind, testCurrentTargetV1Hash, testCurrentTargetV1Hash,
	)
	if err := validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(proof); err != nil {
		t.Fatalf("validate exact host requirements: %v", err)
	}
	for _, digest := range []sql.NullString{{}, {Valid: true, String: "wrong"}} {
		candidate := proof
		candidate.snapshot.entries = append([]applyAllSourceRevisionAdvanceCurrentSnapshotEntry{}, proof.snapshot.entries...)
		candidate.snapshot.entries[0].row.HostRequirementsDigest = digest
		assertApplyAllSourceRevisionAdvancePreconditionError(
			t, validateApplyAllSourceRevisionAdvanceFrozenCurrentTargetFacts(candidate),
			ApplyAllSourceRevisionAdvancePlanState, advance.index, advance.Name,
		)
	}
}

func testApplyAllSourceRevisionAdvanceFrozenCurrentTargetProof(
	t *testing.T,
	advance normalizedApplyAllSourceRevisionAdvance,
	target *packs.SourceBackedPack,
	status string,
	rowKind string,
	applied func(*packs.SourceBackedPack) string,
	desired func(*packs.SourceBackedPack) string,
) applyAllSourceRevisionAdvancePreconditionProof {
	t.Helper()
	meta := packs.PackHashMetadataForPack(target)
	hostDigest, err := packs.PackHostRequirementsDigest(target)
	if err != nil {
		t.Fatalf("host requirements digest: %v", err)
	}
	row := installedPackRow{
		ID: "row-" + advance.Name, Name: advance.Name, PackInstallID: "install-" + advance.Name,
		Category: string(target.Category()), Status: status,
		PackVersion:           sql.NullString{Valid: true, String: meta.PackVersion},
		ManifestFormatVersion: sql.NullString{Valid: true, String: meta.ManifestFormatVersion},
		PlatformCompatibility: sql.NullString{Valid: meta.PlatformCompatibilityValid, String: meta.PlatformCompatibility},
		PayloadHashAlgorithm:  sql.NullString{Valid: true, String: meta.PayloadHashAlgorithm},
		SourceKind:            sql.NullString{Valid: true, String: rowKind}, SourceURI: sql.NullString{Valid: true, String: advance.URI},
		SourceRevision:         sql.NullString{Valid: true, String: meta.SourceRevision},
		HostRequirementsDigest: sql.NullString{Valid: true, String: hostDigest},
	}
	if applied != nil {
		row.AppliedPayloadHash = sql.NullString{Valid: true, String: applied(target)}
	}
	if desired != nil {
		row.DesiredPayloadHash = sql.NullString{Valid: true, String: desired(target)}
	}
	pending, err := classifyApplyAllSourceRevisionAdvance(row, advance.SourceRevisionAdvanceAuthorization)
	if err != nil {
		t.Fatalf("classify frozen row: %v", err)
	}
	return applyAllSourceRevisionAdvancePreconditionProof{
		snapshot: applyAllSourceRevisionAdvanceCurrentSnapshot{entries: []applyAllSourceRevisionAdvanceCurrentSnapshotEntry{{advance: advance, row: row, pending: pending}}},
		current: applyAllSourceRevisionAdvanceTuple{
			plan: []normalizedApplyAllSourceRevisionAdvance{advance}, targets: map[string]*packs.SourceBackedPack{advance.Name: target},
		},
		replay: applyAllSourceRevisionAdvanceTuple{
			plan: []normalizedApplyAllSourceRevisionAdvance{advance}, targets: map[string]*packs.SourceBackedPack{advance.Name: target},
		},
	}
}

func testCurrentTargetV1Hash(target *packs.SourceBackedPack) string {
	hash, _ := packs.PackPayloadHash(target)
	return hash
}

func testCurrentTargetLegacyHash(target *packs.SourceBackedPack) string {
	hash, _ := packs.LegacyPackPayloadHash(target)
	return hash
}
