package controlplane

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

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

func TestValidateInstallPackAttributionPreflightRejectsDifferentPreparedRuntimeBeforeSQL(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	otherDB, otherMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("other sqlmock: %v", err)
	}
	t.Cleanup(func() { _ = otherDB.Close() })
	entries := testApplyAllSourceRevisionAdvanceTwoRetryAttributionEntries(
		t, installedPackStatusFailed, false, false,
	)
	registrations := runnerTestRegistrationSet(t)
	_, overrides := testApplyAllSourceRevisionAdvancePreparedAttribution(
		t, db, entries, registrations,
	)
	wrongRegistrations, err := packs.NewComponentKindRegistrationSet(registrations.Registrations())
	if err != nil {
		t.Fatalf("rebuild registrations: %v", err)
	}
	if registrations.SameSnapshot(wrongRegistrations) {
		t.Fatal("rebuilt registrations unexpectedly retained snapshot identity")
	}
	for _, candidate := range []struct {
		name          string
		db            *sql.DB
		registrations packs.ComponentKindRegistrationSet
		want          string
	}{
		{name: "database", db: otherDB, registrations: registrations, want: "runtime database"},
		{name: "registrations", db: db, registrations: wrongRegistrations, want: "component registrations"},
	} {
		t.Run(candidate.name, func(t *testing.T) {
			err := validateInstallPackAttributionPreflightWithComponentKindRegistrationSet(
				context.Background(), candidate.db, entries[0].installedCompatibility,
				overrides, candidate.registrations,
			)
			if err == nil || !strings.Contains(err.Error(), candidate.want) {
				t.Fatalf("mismatched %s error = %v", candidate.name, err)
			}
		})
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("prepared database was read before authority rejection: %v", err)
	}
	if err := otherMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("different database was read before authority rejection: %v", err)
	}
}

func TestValidateInstallPackAttributionPreflightAllowsProofBoundRetryPeer(t *testing.T) {
	for _, test := range []struct {
		name            string
		peerStatus      string
		selectedDeleted bool
		peerDeleted     bool
		listPeer        bool
	}{
		{name: "listed failed peer", peerStatus: installedPackStatusFailed, listPeer: true},
		{
			name: "soft-deleted selected and installing peer", peerStatus: installedPackStatusInstalling,
			selectedDeleted: true, peerDeleted: true,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("sqlmock: %v", err)
			}
			t.Cleanup(func() { _ = db.Close() })
			entries := testApplyAllSourceRevisionAdvanceTwoRetryAttributionEntries(
				t, test.peerStatus, test.selectedDeleted, test.peerDeleted, "BetaOwned",
			)
			prepared, overrides := testApplyAllSourceRevisionAdvancePreparedAttribution(t, db, entries)
			peerRow := entries[1].current.row
			expectApplyAllSourceRevisionAdvancePreconditionRow(mock, peerRow.Name, peerRow)
			if test.listPeer {
				expectApplyAllSourceRevisionAdvanceAttributionInstalledPackListRows(mock, peerRow)
			} else {
				expectApplyAllSourceRevisionAdvanceAttributionInstalledPackListRows(mock)
			}
			allowedObjects, err := AllowedL0ObjectAPINames(context.Background(), db)
			if err != nil {
				t.Fatalf("load expected L0 object oracle: %v", err)
			}
			allowedObjects["betaowned"] = struct{}{}
			allowedPacks := map[string]struct{}{"alpha": {}, "beta": {}}
			for _, runtimePack := range packs.RuntimeRequiredPacks() {
				allowedPacks[strings.ToLower(runtimePack.Name())] = struct{}{}
			}
			expectSuccessfulApplyAllSourceRevisionAdvanceAttributionSurfaceChecks(
				mock, sortedKeys(allowedObjects), sortedKeys(allowedPacks),
			)

			err = validateInstallPackAttributionPreflightWithComponentKindRegistrationSet(
				context.Background(), db, entries[0].installedCompatibility, overrides,
				prepared.admission.identity.proof.executionRuntime.componentKindRegistrationSet,
			)
			if err != nil {
				t.Fatalf("proof-bound retry peer attribution preflight: %v", err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("retry-peer SQL expectations: %v", err)
			}
		})
	}
}

func TestValidateInstallPackAttributionPreflightRejectsUnrelatedFailedPeerWithAuthority(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	entries := testApplyAllSourceRevisionAdvanceTwoRetryAttributionEntries(
		t, installedPackStatusFailed, false, false,
	)
	prepared, overrides := testApplyAllSourceRevisionAdvancePreparedAttribution(t, db, entries)
	peerRow := entries[1].current.row
	foreignRow := peerRow
	foreignRow.ID = "gamma-id"
	foreignRow.Name = "gamma"
	foreignRow.PackInstallID = "gamma-install-id"
	expectApplyAllSourceRevisionAdvancePreconditionRow(mock, peerRow.Name, peerRow)
	expectApplyAllSourceRevisionAdvanceAttributionInstalledPackListRows(mock, peerRow, foreignRow)
	expectInstalledPackStateLookup(mock, foreignRow.Name, installedPackStatusFailed, 0)
	expectInstalledPackStateLookup(mock, foreignRow.Name, installedPackStatusFailed, 0)

	err = validateInstallPackAttributionPreflightWithComponentKindRegistrationSet(
		context.Background(), db, entries[0].installedCompatibility, overrides,
		prepared.admission.identity.proof.executionRuntime.componentKindRegistrationSet,
	)
	if err == nil || !strings.Contains(err.Error(), "gamma") ||
		!strings.Contains(err.Error(), `status is "failed"`) {
		t.Fatalf("unrelated failed peer error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unrelated failed peer SQL expectations: %v", err)
	}
}

func TestValidateInstallPackAttributionPreflightRejectsChangedProofBoundRetryPeer(t *testing.T) {
	for _, test := range []struct {
		name   string
		change func(*installedPackRow)
	}{
		{name: "incarnation", change: func(row *installedPackRow) { row.PackInstallID += "-changed" }},
		{name: "active", change: func(row *installedPackRow) { row.Status = installedPackStatusActive }},
		{name: "removing", change: func(row *installedPackRow) { row.Status = installedPackStatusRemoving }},
		{name: "soft deleted", change: func(row *installedPackRow) { row.IsDeleted = 1 }},
	} {
		t.Run(test.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("sqlmock: %v", err)
			}
			t.Cleanup(func() { _ = db.Close() })
			entries := testApplyAllSourceRevisionAdvanceTwoRetryAttributionEntries(
				t, installedPackStatusFailed, false, false,
			)
			prepared, overrides := testApplyAllSourceRevisionAdvancePreparedAttribution(t, db, entries)
			peerRow := entries[1].current.row
			changed := peerRow
			test.change(&changed)
			expectApplyAllSourceRevisionAdvancePreconditionRow(mock, peerRow.Name, changed)

			err = validateInstallPackAttributionPreflightWithComponentKindRegistrationSet(
				context.Background(), db, entries[0].installedCompatibility, overrides,
				prepared.admission.identity.proof.executionRuntime.componentKindRegistrationSet,
			)
			if err == nil || !strings.Contains(err.Error(), "changed after execution admission") {
				t.Fatalf("changed proof-bound retry peer error = %v", err)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("changed-peer SQL expectations: %v", err)
			}
		})
	}
}

func testApplyAllSourceRevisionAdvanceTwoRetryAttributionEntries(
	t *testing.T,
	peerStatus string,
	selectedDeleted bool,
	peerDeleted bool,
	peerOwnedObjects ...string,
) []applyAllSourceRevisionAdvanceExecutionProofEntry {
	t.Helper()
	entries := []applyAllSourceRevisionAdvanceExecutionProofEntry{
		testApplyAllSourceRevisionAdvanceAttributionEntry(
			t, "alpha", 3, installedPackStatusFailed,
			ApplyAllSourceRevisionAdvancePendingRetry,
		),
		testApplyAllSourceRevisionAdvanceAttributionEntry(
			t, "beta", 11, peerStatus,
			ApplyAllSourceRevisionAdvancePendingRetry, peerOwnedObjects...,
		),
	}
	if selectedDeleted {
		entries[0].current.row.IsDeleted = 1
	}
	if peerDeleted {
		entries[1].current.row.IsDeleted = 1
	}
	return entries
}

func expectApplyAllSourceRevisionAdvanceAttributionInstalledPackListRows(
	mock sqlmock.Sqlmock,
	rows ...installedPackRow,
) {
	expectInstalledPackRuntimeDeclarationColumns(mock, false)
	result := sqlmock.NewRows([]string{
		"Name", "Category", "Status", "PackVersion", "ManifestFormatVersion", "PlatformCompatibility",
		"DesiredPayloadHash", "AppliedPayloadHash", "PayloadHashAlgorithm", "SourceKind", "SourceURI", "SourceRevision", "HostRequirementsDigest",
	})
	for _, row := range rows {
		result.AddRow(
			row.Name, row.Category, row.Status,
			nullStringArg(row.PackVersion), nullStringArg(row.ManifestFormatVersion), nullStringArg(row.PlatformCompatibility),
			nullStringArg(row.DesiredPayloadHash), nullStringArg(row.AppliedPayloadHash), nullStringArg(row.PayloadHashAlgorithm),
			nullStringArg(row.SourceKind), nullStringArg(row.SourceURI), nullStringArg(row.SourceRevision),
			nullStringArg(row.HostRequirementsDigest),
		)
	}
	mock.ExpectQuery("SELECT Name, Category, Status, PackVersion, ManifestFormatVersion, PlatformCompatibility, DesiredPayloadHash, AppliedPayloadHash, PayloadHashAlgorithm, SourceKind, SourceURI, SourceRevision").
		WithArgs(string(packs.AppPack), string(packs.ConnectorPack)).
		WillReturnRows(result)
}

func expectSuccessfulApplyAllSourceRevisionAdvanceAttributionSurfaceChecks(
	mock sqlmock.Sqlmock,
	allowedObjects []string,
	allowedPacks []string,
) {
	args := make([]driver.Value, len(allowedObjects))
	for index, objectAPIName := range allowedObjects {
		args[index] = objectAPIName
	}
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM _System_Object o WHERE").
		WithArgs(args...).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM _System_Table t WHERE").
		WithArgs(args...).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM _System_Field f JOIN _System_Object o ON o.Id = f.ObjectId WHERE").
		WithArgs(args...).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
	packArgs := make([]driver.Value, len(allowedPacks))
	for index, packName := range allowedPacks {
		packArgs[index] = packName
	}
	expectZeroInstallOwnerSurfaceCounts(mock, allowedPacks)
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM _System_Field f JOIN _System_Object o ON o.Id = f.ObjectId").
		WithArgs(packArgs...).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM _System_Field f JOIN _System_Object o ON o.Id = f.ObjectId").
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
	mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM FlowDefinition fd").
		WithArgs(packArgs...).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
}
