package controlplane

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/db"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

func TestValidateApplyAllSourceRevisionAdvanceCurrentPayloadsByPendingKind(t *testing.T) {
	advanceV1 := testApplyAllSourceRevisionAdvance("advance-v1", 3)
	advanceLegacy := testApplyAllSourceRevisionAdvance("advance-legacy", 5)
	retry := testApplyAllSourceRevisionAdvance("retry", 7)
	canonicalize := testApplyAllSourceRevisionAdvance("canonicalize", 9)
	current := testApplyAllSourceRevisionAdvance("current", 11)
	plan := []normalizedApplyAllSourceRevisionAdvance{advanceV1, advanceLegacy, retry, canonicalize, current}
	targets := map[string]*packs.SourceBackedPack{
		advanceV1.Name:     testApplyAllSourceRevisionTarget(t, advanceV1.Name, "1.0.0", advanceV1.Kind, advanceV1.URI, advanceV1.FromRevision),
		advanceLegacy.Name: testApplyAllSourceRevisionTarget(t, advanceLegacy.Name, "1.0.0", advanceLegacy.Kind, advanceLegacy.URI, advanceLegacy.FromRevision),
		retry.Name:         testApplyAllSourceRevisionTarget(t, retry.Name, "1.0.0", retry.Kind, retry.URI, retry.ToRevision),
		canonicalize.Name:  testApplyAllSourceRevisionTarget(t, canonicalize.Name, "1.0.0", canonicalize.Kind, canonicalize.URI, canonicalize.ToRevision),
		current.Name:       testApplyAllSourceRevisionTarget(t, current.Name, "1.0.0", current.Kind, current.URI, current.ToRevision),
	}
	v1Hash, err := packs.PackPayloadHash(targets[advanceV1.Name])
	if err != nil {
		t.Fatalf("v1 payload hash: %v", err)
	}
	legacyHash, err := packs.LegacyPackPayloadHash(targets[advanceLegacy.Name])
	if err != nil {
		t.Fatalf("legacy payload hash: %v", err)
	}
	snapshot := applyAllSourceRevisionAdvanceCurrentSnapshot{entries: []applyAllSourceRevisionAdvanceCurrentSnapshotEntry{
		{advance: advanceV1, row: testApplyAllSourceRevisionAdvancePreconditionRow(advanceV1.Name, installedPackStatusActive, advanceV1.Kind, advanceV1.FromRevision, v1Hash), pending: testApplyAllSourceRevisionAdvancePending(ApplyAllSourceRevisionAdvancePendingAdvance)},
		{advance: advanceLegacy, row: testApplyAllSourceRevisionAdvancePreconditionRow(advanceLegacy.Name, installedPackStatusActive, advanceLegacy.Kind, advanceLegacy.FromRevision, legacyHash), pending: testApplyAllSourceRevisionAdvancePending(ApplyAllSourceRevisionAdvancePendingAdvance)},
		{advance: retry, row: testApplyAllSourceRevisionAdvancePreconditionRow(retry.Name, installedPackStatusFailed, retry.Kind, retry.ToRevision, ""), pending: testApplyAllSourceRevisionAdvancePending(ApplyAllSourceRevisionAdvancePendingRetry)},
		{advance: canonicalize, row: testApplyAllSourceRevisionAdvancePreconditionRow(canonicalize.Name, installedPackStatusActive, "in_repo", canonicalize.ToRevision, "stale-hash"), pending: testApplyAllSourceRevisionAdvancePending(ApplyAllSourceRevisionAdvancePendingCanonicalize)},
		{advance: current, row: testApplyAllSourceRevisionAdvancePreconditionRow(current.Name, installedPackStatusActive, current.Kind, current.ToRevision, "")},
	}}
	tuple := applyAllSourceRevisionAdvanceTuple{plan: plan, targets: targets}
	if err := validateApplyAllSourceRevisionAdvanceCurrentPayloads(snapshot, tuple); err != nil {
		t.Fatalf("validate current payloads: %v", err)
	}

	tests := []struct {
		name  string
		entry int
		edit  func(*applyAllSourceRevisionAdvanceCurrentSnapshotEntry)
	}{
		{name: "empty v1 advance hash", entry: 0, edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.row.AppliedPayloadHash = sql.NullString{}
		}},
		{name: "mismatched legacy advance hash", entry: 1, edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.row.AppliedPayloadHash = sql.NullString{Valid: true, String: strings.Repeat("0", 64)}
		}},
		{name: "unknown pending kind", entry: 2, edit: func(entry *applyAllSourceRevisionAdvanceCurrentSnapshotEntry) {
			entry.pending = testApplyAllSourceRevisionAdvancePending("unknown")
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			candidate := applyAllSourceRevisionAdvanceCurrentSnapshot{entries: append([]applyAllSourceRevisionAdvanceCurrentSnapshotEntry(nil), snapshot.entries...)}
			test.edit(&candidate.entries[test.entry])
			err := validateApplyAllSourceRevisionAdvanceCurrentPayloads(candidate, tuple)
			assertApplyAllSourceRevisionAdvancePreconditionError(t, err, ApplyAllSourceRevisionAdvancePlanState, plan[test.entry].index, plan[test.entry].Name)
		})
	}
}

func TestProveApplyAllSourceRevisionAdvancePreconditionsBuildsFrozenCurrentTuple(t *testing.T) {
	empty, err := proveApplyAllSourceRevisionAdvancePreconditions(context.Background(), nil, nil)
	if err != nil || len(empty.snapshot.entries) != 0 || len(empty.current.plan) != 0 || len(empty.current.targets) != 0 {
		t.Fatalf("empty proof = %#v, %v", empty, err)
	}

	advance := testApplyAllSourceRevisionAdvance("proof-pack", 13)
	target := testApplyAllSourceRevisionTarget(t, advance.Name, "1.0.0", advance.Kind, advance.URI, advance.FromRevision)
	payloadHash, err := packs.PackPayloadHash(target)
	if err != nil {
		t.Fatalf("payload hash: %v", err)
	}
	row := testApplyAllSourceRevisionAdvancePreconditionRow(advance.Name, installedPackStatusActive, advance.Kind, advance.FromRevision, payloadHash)
	rawDB, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	t.Cleanup(func() { _ = rawDB.Close() })
	expectApplyAllSourceRevisionAdvancePreconditionRow(mock, advance.Name, row)
	expectApplyAllSourceRevisionAdvanceActiveRuntimeObservation(mock)

	originalValidator := validateApplyAllSourceRevisionSource
	originalResolver := resolveApplyAllSourceRevisionInstalledTarget
	originalFormulaResolver := resolveApplyAllSourceRevisionFormulaContext
	t.Cleanup(func() {
		validateApplyAllSourceRevisionSource = originalValidator
		resolveApplyAllSourceRevisionInstalledTarget = originalResolver
		resolveApplyAllSourceRevisionFormulaContext = originalFormulaResolver
	})
	events := []string{}
	validateApplyAllSourceRevisionSource = func(string, packs.PackSourceOptions, packs.ComponentKindRegistrationSet) packs.PackValidationReport {
		if err := mock.ExpectationsWereMet(); err != nil {
			t.Fatalf("active runtime observation was not frozen before source resolution: %v", err)
		}
		events = append(events, "source")
		return packs.PackValidationReport{Source: testApplyAllSourceRevisionSource(advance.Name, "1.0.0", advance.FromRevision)}
	}
	resolveApplyAllSourceRevisionFormulaContext = func(
		_ context.Context,
		snapshot *packs.ActiveInstalledRuntimeSnapshot,
		installing []packs.Pack,
		_ packs.ComponentKindRegistrationSet,
	) ([]formula.FunctionBundle, error) {
		if snapshot == nil {
			t.Fatal("formula context snapshot = nil, want frozen active-runtime snapshot")
		}
		if len(installing) != 1 || installing[0].Name() != advance.Name {
			t.Fatalf("formula installing tuple = %#v, want %s", installing, advance.Name)
		}
		events = append(events, "formula")
		return []formula.FunctionBundle{{Functions: []formula.FunctionDefinition{{Name: "proof-sentinel"}}}}, nil
	}
	resolveApplyAllSourceRevisionInstalledTarget = func(context.Context, *sql.DB, installedPackRow, string, packs.PackSourceOptions, []formula.FunctionBundle, bool, packs.ComponentKindRegistrationSet, ...packs.SourceBackedPackOption) (*packs.SourceBackedPack, error) {
		events = append(events, "installed")
		return target, nil
	}

	proof, err := proveApplyAllSourceRevisionAdvancePreconditions(
		context.Background(), &Runner{DB: database.NewConnectionFromDB(rawDB)}, []normalizedApplyAllSourceRevisionAdvance{advance},
	)
	if err != nil {
		t.Fatalf("prove preconditions: %v", err)
	}
	if strings.Join(events, ",") != "source,formula,installed" ||
		!proof.activeRuntime.minted || proof.activeRuntime.identity == nil || proof.activeRuntime.identity.db != rawDB ||
		proof.activeRuntime.identity.snapshot == nil ||
		len(proof.snapshot.entries) != 1 || proof.snapshot.entries[0].row != row || proof.current.targets[advance.Name] != target {
		t.Fatalf("proof events/runtime/snapshot/current = %v/%#v/%#v/%#v", events, proof.activeRuntime, proof.snapshot, proof.current)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("SQL expectations: %v", err)
	}
}

func testApplyAllSourceRevisionAdvancePending(kind ApplyAllSourceRevisionAdvancePendingKind) *PendingApplyAllSourceRevisionAdvance {
	return &PendingApplyAllSourceRevisionAdvance{Kind: kind}
}

func testApplyAllSourceRevisionAdvancePreconditionRow(name, status, kind, revision, appliedHash string) installedPackRow {
	return installedPackRow{
		ID: "row-" + name, Name: name, PackInstallID: "install-" + name, Category: string(packs.AppPack), Status: status,
		PackVersion: sql.NullString{Valid: true, String: "1.0.0"}, ManifestFormatVersion: sql.NullString{Valid: true, String: packs.SupportedManifestFormatVersion},
		PlatformCompatibility: sql.NullString{Valid: true, String: "compatibility"}, DesiredPayloadHash: sql.NullString{Valid: true, String: "desired"},
		AppliedPayloadHash: sql.NullString{Valid: appliedHash != "", String: appliedHash}, PayloadHashAlgorithm: sql.NullString{Valid: true, String: packs.PackPayloadHashAlgorithmV1},
		SourceKind: sql.NullString{Valid: true, String: kind}, SourceURI: sql.NullString{Valid: true, String: name}, SourceRevision: sql.NullString{Valid: true, String: revision},
		HostRequirementsDigest: sql.NullString{Valid: true, String: "host-requirements"},
	}
}

func expectApplyAllSourceRevisionAdvancePreconditionRow(mock sqlmock.Sqlmock, queryName string, row installedPackRow) {
	mock.ExpectQuery(applyAllSourceRevisionAdvanceRowQueryPattern()).WithArgs(queryName).WillReturnRows(
		testApplyAllSourceRevisionAdvancePreconditionRows().AddRow(
			row.ID, row.Name, row.PackInstallID, row.Category, row.Status, row.IsDeleted,
			testApplyAllSourceRevisionAdvanceNullValue(row.PackVersion), testApplyAllSourceRevisionAdvanceNullValue(row.ManifestFormatVersion), testApplyAllSourceRevisionAdvanceNullValue(row.PlatformCompatibility),
			testApplyAllSourceRevisionAdvanceNullValue(row.DesiredPayloadHash), testApplyAllSourceRevisionAdvanceNullValue(row.AppliedPayloadHash), testApplyAllSourceRevisionAdvanceNullValue(row.PayloadHashAlgorithm),
			testApplyAllSourceRevisionAdvanceNullValue(row.SourceKind), testApplyAllSourceRevisionAdvanceNullValue(row.SourceURI), testApplyAllSourceRevisionAdvanceNullValue(row.SourceRevision), testApplyAllSourceRevisionAdvanceNullValue(row.HostRequirementsDigest),
		),
	)
}

func testApplyAllSourceRevisionAdvancePreconditionRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"Id", "Name", "PackInstallId", "Category", "Status", "IsDeleted", "PackVersion", "ManifestFormatVersion", "PlatformCompatibility",
		"DesiredPayloadHash", "AppliedPayloadHash", "PayloadHashAlgorithm", "SourceKind", "SourceURI", "SourceRevision", "HostRequirementsDigest",
	})
}

func testApplyAllSourceRevisionAdvanceNullValue(value sql.NullString) any {
	if !value.Valid {
		return nil
	}
	return value.String
}

func assertApplyAllSourceRevisionAdvancePreconditionError(t *testing.T, err error, stage ApplyAllSourceRevisionAdvancePlanErrorStage, index int, name string) {
	t.Helper()
	var planErr *ApplyAllSourceRevisionAdvancePlanError
	if !errors.As(err, &planErr) || planErr.Stage != stage || planErr.Index != index || planErr.Name != name {
		t.Fatalf("error = %#v, want stage=%q index=%d name=%q", err, stage, index, name)
	}
}
