package controlplane

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

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

func TestFailApplyAllSourceRevisionAdvanceAttemptUsesExactFreshIncarnation(t *testing.T) {
	for _, test := range []struct {
		name      string
		category  packs.Category
		pending   ApplyAllSourceRevisionAdvancePendingKind
		status    string
		queue     string
		cleanup   bool
		reconcile bool
	}{
		{name: "app advance", category: packs.AppPack, pending: ApplyAllSourceRevisionAdvancePendingAdvance, status: installedPackStatusActive, queue: "e.CompanionPackInstallId"},
		{name: "connector advance", category: packs.ConnectorPack, pending: ApplyAllSourceRevisionAdvancePendingAdvance, status: installedPackStatusActive, queue: "e.InstalledPackId", cleanup: true, reconcile: true},
		{name: "app retry", category: packs.AppPack, pending: ApplyAllSourceRevisionAdvancePendingRetry, status: installedPackStatusFailed, queue: "e.CompanionPackInstallId"},
		{name: "connector retry", category: packs.ConnectorPack, pending: ApplyAllSourceRevisionAdvancePendingRetry, status: installedPackStatusInstalling, queue: "e.InstalledPackId", cleanup: true, reconcile: true},
	} {
		t.Run(test.name, func(t *testing.T) {
			queries := []string{}
			matcher := sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
				if err := sqlmock.QueryMatcherRegexp.Match(expectedSQL, actualSQL); err != nil {
					return err
				}
				queries = append(queries, actualSQL)
				return nil
			})
			db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(matcher))
			if err != nil {
				t.Fatalf("sqlmock: %v", err)
			}
			t.Cleanup(func() { _ = db.Close() })
			db.SetMaxOpenConns(1)
			attempt := testBeginApplyAllSourceRevisionAdvanceFailureAttemptFor(
				t, db, mock, test.category, test.pending, test.status,
			)
			installing := attempt.identity.installingRow

			expectApplyAllSourceRevisionAdvanceExactIncarnationBegin(mock, installing, test.category, false)
			mock.ExpectExec("UPDATE _System_InstalledPack p").
				WillReturnResult(sqlmock.NewResult(0, 1))
			if test.cleanup {
				expectApplyAllSourceRevisionAdvanceFailureConnectorCleanup(mock, nil)
			}
			expectApplyAllSourceRevisionAdvanceExactIncarnationCommit(mock)

			reconciled := false
			restore := SetConnectorInboundWorkerReconciler(func(_ context.Context, got *sql.DB) error {
				if got != db {
					return fmt.Errorf("reconciler database = %p, want %p", got, db)
				}
				if err := mock.ExpectationsWereMet(); err != nil {
					return fmt.Errorf("reconciler ran before exact transaction completed: %w", err)
				}
				reconciled = true
				return nil
			})
			t.Cleanup(restore)
			if err := failApplyAllSourceRevisionAdvanceAttempt(
				context.Background(), attempt, errors.New("apply failed"),
			); err != nil {
				t.Fatalf("fail exact attempt: %v", err)
			}
			if reconciled != test.reconcile {
				t.Fatalf("connector worker reconciled = %t, want %t", reconciled, test.reconcile)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("SQL expectations: %v", err)
			}

			update := testApplyAllSourceRevisionAdvanceFailureUpdateQuery(t, queries)
			for _, want := range []string{
				"Status = ?", "LastError = ?", "LastModifiedDate = ?", "IsDeleted = 0",
				applyAllSourceRevisionAdvanceExactStringPredicate("p.Id"),
				applyAllSourceRevisionAdvanceExactStringPredicate("p.PackInstallId"),
				testApplyAllSourceRevisionAdvanceExactQueuePackMatch(test.category),
				"FROM _System_ConnectorWorkItem w",
				"ProjectionKey",
			} {
				if !strings.Contains(update, want) {
					t.Fatalf("exact failure update missing %q:\n%s", want, update)
				}
			}
			for _, forbidden := range []string{
				"SET PackInstallId =", "AppliedPayloadHash =", "DesiredPayloadHash =",
				"LastAppliedDate =", "SourceRevision =", "INSERT INTO",
			} {
				if strings.Contains(update, forbidden) {
					t.Fatalf("exact failure update mutates preserved state with %q:\n%s", forbidden, update)
				}
			}
		})
	}
}

func TestFailApplyAllSourceRevisionAdvanceAttemptRequiresExactlyOneRow(t *testing.T) {
	for _, affected := range []int64{0, 2} {
		t.Run(fmt.Sprintf("%d rows", affected), func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("sqlmock: %v", err)
			}
			t.Cleanup(func() { _ = db.Close() })
			db.SetMaxOpenConns(1)
			attempt := testBeginApplyAllSourceRevisionAdvanceFailureAttempt(t, db, mock, packs.AppPack)
			expectApplyAllSourceRevisionAdvanceExactIncarnationBegin(
				mock, attempt.identity.installingRow, packs.AppPack, false,
			)
			mock.ExpectExec("UPDATE _System_InstalledPack p").
				WillReturnResult(sqlmock.NewResult(0, affected))
			expectApplyAllSourceRevisionAdvanceExactIncarnationRollback(mock)

			err = failApplyAllSourceRevisionAdvanceAttempt(
				context.Background(), attempt, errors.New("apply failed"),
			)
			entry := attempt.identity.admission.identity.proof.entries[attempt.identity.selectedProofIndex]
			assertApplyAllSourceRevisionAdvancePreconditionError(
				t, err, ApplyAllSourceRevisionAdvancePlanState,
				entry.current.advance.index,
				entry.current.advance.Name,
			)
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("SQL expectations: %v", err)
			}
		})
	}
}

func TestFailApplyAllSourceRevisionAdvanceAttemptRollsBackConnectorCleanup(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	db.SetMaxOpenConns(1)
	attempt := testBeginApplyAllSourceRevisionAdvanceFailureAttempt(t, db, mock, packs.ConnectorPack)
	expectApplyAllSourceRevisionAdvanceExactIncarnationBegin(
		mock, attempt.identity.installingRow, packs.ConnectorPack, false,
	)
	mock.ExpectExec("UPDATE _System_InstalledPack p").WillReturnResult(sqlmock.NewResult(0, 1))
	expectApplyAllSourceRevisionAdvanceFailureConnectorCleanup(mock, errors.New("cleanup failed"))
	expectApplyAllSourceRevisionAdvanceExactIncarnationRollback(mock)

	reconciled := false
	restore := SetConnectorInboundWorkerReconciler(func(context.Context, *sql.DB) error {
		reconciled = true
		return nil
	})
	t.Cleanup(restore)
	err = failApplyAllSourceRevisionAdvanceAttempt(
		context.Background(), attempt, errors.New("apply failed"),
	)
	if err == nil || !strings.Contains(err.Error(), "cleanup failed") {
		t.Fatalf("cleanup failure error = %v", err)
	}
	if reconciled {
		t.Fatal("connector workers reconciled after rolled-back failure")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("SQL expectations: %v", err)
	}
}

func TestFailApplyAllSourceRevisionAdvanceAttemptRejectsInvalidIdentityBeforeSQL(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	t.Cleanup(func() { _ = db.Close() })
	db.SetMaxOpenConns(1)
	valid := testBeginApplyAllSourceRevisionAdvanceFailureAttempt(t, db, mock, packs.AppPack)
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("begin SQL expectations: %v", err)
	}

	for _, test := range []struct {
		name     string
		attempt  applyAllSourceRevisionAdvanceAttempt
		applyErr error
		want     string
	}{
		{name: "missing identity", attempt: applyAllSourceRevisionAdvanceAttempt{}, applyErr: errors.New("failed"), want: "attempt is not an admission-bound capability"},
		{name: "unminted", attempt: testApplyAllSourceRevisionAdvanceFailureAttemptMutation(valid, func(identity *applyAllSourceRevisionAdvanceAttemptIdentity) {
			identity.minted = false
		}), applyErr: errors.New("failed"), want: "attempt was not minted"},
		{name: "reused incarnation", attempt: testApplyAllSourceRevisionAdvanceFailureAttemptMutation(valid, func(identity *applyAllSourceRevisionAdvanceAttemptIdentity) {
			entry := identity.admission.identity.proof.entries[identity.selectedProofIndex]
			identity.installingRow.PackInstallID = entry.current.row.PackInstallID
			identity.provenance.installingRow.PackInstallID = entry.current.row.PackInstallID
		}), applyErr: errors.New("failed"), want: "fresh installed-row incarnation"},
		{name: "not installing", attempt: testApplyAllSourceRevisionAdvanceFailureAttemptMutation(valid, func(identity *applyAllSourceRevisionAdvanceAttemptIdentity) {
			identity.installingRow.Status = installedPackStatusActive
			identity.provenance.installingRow.Status = installedPackStatusActive
		}), applyErr: errors.New("failed"), want: "live installing incarnation"},
		{name: "coordinated selected-atom claim substitution", attempt: testApplyAllSourceRevisionAdvanceFailureAttemptMutation(valid, func(identity *applyAllSourceRevisionAdvanceAttemptIdentity) {
			replacement := &applyAllSourceRevisionAdvanceSelectedAtomClaim{}
			identity.selectedAtomClaim = replacement
			identity.provenance.selectedAtomClaim = replacement
		}), applyErr: errors.New("failed"), want: "outer witness"},
		{name: "missing apply error", attempt: valid, want: "apply error is required"},
	} {
		t.Run(test.name, func(t *testing.T) {
			err := failApplyAllSourceRevisionAdvanceAttempt(
				context.Background(), test.attempt, test.applyErr,
			)
			if err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("invalid attempt error = %v, want %q before SQL", err, test.want)
			}
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("invalid attempt reached SQL: %v", err)
			}
		})
	}
}

func TestApplyAllSourceRevisionAdvanceFailureMessageIsBounded(t *testing.T) {
	message := applyAllSourceRevisionAdvanceFailureMessage(errors.New(strings.Repeat("x", 5000)))
	if len(message) != 4000 {
		t.Fatalf("failure message length = %d, want 4000", len(message))
	}
}

func testBeginApplyAllSourceRevisionAdvanceFailureAttempt(
	t *testing.T,
	db *sql.DB,
	mock sqlmock.Sqlmock,
	category packs.Category,
) applyAllSourceRevisionAdvanceAttempt {
	t.Helper()
	return testBeginApplyAllSourceRevisionAdvanceFailureAttemptFor(
		t, db, mock, category,
		ApplyAllSourceRevisionAdvancePendingAdvance, installedPackStatusActive,
	)
}

func testBeginApplyAllSourceRevisionAdvanceFailureAttemptFor(
	t *testing.T,
	db *sql.DB,
	mock sqlmock.Sqlmock,
	category packs.Category,
	pending ApplyAllSourceRevisionAdvancePendingKind,
	status string,
) applyAllSourceRevisionAdvanceAttempt {
	t.Helper()
	admission := testApplyAllSourceRevisionAdvanceBeginAdmission(
		t, db, category, pending, status,
	)
	oldRow := admission.identity.proof.entries[0].current.row
	receipt := testApplyAllSourceRevisionAdvanceBeginReceipt(
		t, context.Background(), db, mock, admission, 0, packs.ApplyOptions{},
	)
	expectApplyAllSourceRevisionAdvanceExactIncarnationBegin(mock, oldRow, category, false)
	if category == packs.ConnectorPack {
		mock.ExpectQuery("SELECT ProtocolVersion,LegacyMaxOrdinal").WithArgs(oldRow.PackInstallID).
			WillReturnRows(sqlmock.NewRows([]string{"ProtocolVersion", "LegacyMaxOrdinal"}).AddRow(1, 0))
	}
	mock.ExpectExec("UPDATE _System_InstalledPack p").WillReturnResult(sqlmock.NewResult(0, 1))
	expectApplyAllSourceRevisionAdvanceExactIncarnationCommit(mock)
	attempt, err := beginApplyAllSourceRevisionAdvanceAttempt(
		context.Background(), receipt,
	)
	if err != nil {
		t.Fatalf("begin exact attempt: %v", err)
	}
	return attempt
}

func expectApplyAllSourceRevisionAdvanceFailureConnectorCleanup(
	mock sqlmock.Sqlmock,
	firstError error,
) {
	capability := mock.ExpectExec("UPDATE _System_ConnectorCapabilityDeclaration")
	if firstError != nil {
		capability.WillReturnError(firstError)
	} else {
		capability.WillReturnResult(sqlmock.NewResult(0, 1))
	}
	mock.ExpectExec("UPDATE _System_ConnectorRateLimit").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE _System_ConnectorInboundHandlerConfig").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE _System_ConnectorCalloutProfile").WillReturnResult(sqlmock.NewResult(0, 1))
}

func testApplyAllSourceRevisionAdvanceFailureAttemptMutation(
	attempt applyAllSourceRevisionAdvanceAttempt,
	mutate func(*applyAllSourceRevisionAdvanceAttemptIdentity),
) applyAllSourceRevisionAdvanceAttempt {
	identity := *attempt.identity
	identity.admission = cloneApplyAllSourceRevisionAdvanceExecutionAdmission(attempt.identity.admission)
	identity.provenance = attempt.identity.provenance
	identity.provenance.admissionIdentity = identity.admission.identity
	identity.provenance.admissionProvenance = cloneApplyAllSourceRevisionAdvanceExecutionProofProvenance(
		attempt.identity.provenance.admissionProvenance,
	)
	mutate(&identity)
	return applyAllSourceRevisionAdvanceAttempt{
		identity: &identity,
		witness:  attempt.witness,
	}
}

func testApplyAllSourceRevisionAdvanceFailureUpdateQuery(t *testing.T, queries []string) string {
	t.Helper()
	for _, query := range queries {
		if strings.Contains(query, "UPDATE _System_InstalledPack p") &&
			strings.Contains(query, "LastError = ?") {
			return query
		}
	}
	t.Fatalf("exact failure update was not executed; queries: %#v", queries)
	return ""
}
