package catalogtenant

import (
	"context"
	"errors"
	"reflect"
	"regexp"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	tenantauthorityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/tenantlayoutbundleauthorityv1"
)

var (
	namespaceReservationCurrentInventoryQuery = regexp.QuoteMeta(`SELECT TenantNamespaceId, ValueVersion, State,
ValueDigest, LastCommandId
FROM _System_CatalogTenantNamespaceReservation
ORDER BY TenantNamespaceId FOR UPDATE`)
	namespaceReservationHistoryInventoryQuery = regexp.QuoteMeta(`SELECT CommandId, CommandSequence,
TenantNamespaceId, ValueVersion, State, ValueDigest, PriorValueDigest
FROM _System_CatalogTenantNamespaceHistory
ORDER BY TenantNamespaceId, CommandSequence, CommandId FOR UPDATE`)
	appliedNamespaceCommandInventoryQuery = regexp.QuoteMeta(`SELECT CommandId, CommandSequence
FROM _System_CatalogTenantAuthorityCommand
WHERE CommandKind = ? AND Result = ?
ORDER BY CommandSequence, CommandId FOR UPDATE`)
)

func TestInspectNamespaceReservationLineageReturnsActiveSnapshotAndRollsBack(t *testing.T) {
	authority, mock, closeDB := mockTenantAuthority(t)
	defer closeDB()
	fixture := activeReservationObservationFixture(t)

	expectObservationStart(mock, fixture.fenceSequence)
	expectObservationScans(mock, fixture.current, fixture.history, fixture.commands)
	for _, command := range fixture.commands {
		expectCommandRow(mock, command)
	}
	mock.ExpectRollback()

	entries, err := authority.InspectNamespaceReservationLineage(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	want := []ObservedNamespaceReservation{{
		NamespaceID: testNamespace,
		State:       tenantauthorityv1.NamespaceStateActive,
	}}
	if !reflect.DeepEqual(entries, want) {
		t.Fatalf("entries = %#v, want %#v", entries, want)
	}
	assertExpectations(t, mock)
}

func TestInspectNamespaceReservationLineageReturnsTombstoneSnapshot(t *testing.T) {
	authority, mock, closeDB := mockTenantAuthority(t)
	defer closeDB()
	fixture := tombstonedReservationObservationFixture(t)

	expectObservationStart(mock, fixture.fenceSequence)
	expectObservationScans(mock, fixture.current, fixture.history, fixture.commands)
	for _, command := range fixture.commands {
		expectCommandRow(mock, command)
	}
	mock.ExpectRollback()

	entries, err := authority.InspectNamespaceReservationLineage(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	want := []ObservedNamespaceReservation{{
		NamespaceID: testNamespace,
		State:       tenantauthorityv1.NamespaceStateTombstone,
	}}
	if !reflect.DeepEqual(entries, want) {
		t.Fatalf("entries = %#v, want %#v", entries, want)
	}
	assertExpectations(t, mock)
}

func TestInspectNamespaceReservationLineageRejectsSyntheticTombstone(t *testing.T) {
	authority, mock, closeDB := mockTenantAuthority(t)
	defer closeDB()
	tombstone := namespaceTombstone(t, testNamespace)
	command := completeNamespaceObservationCommand(t, testCommand2, tombstone, 2)
	digest := mustNamespaceDigest(t, tombstone.DesiredNamespaceValue)
	fixture := namespaceReservationObservationFixture{
		fenceSequence: 2,
		current: namespaceCurrent{
			Value:         tombstone.DesiredNamespaceValue,
			Digest:        digest,
			LastCommandID: testCommand2,
		},
		history: []namespaceReservationHistory{{
			commandID: testCommand2,
			sequence:  2,
			value:     tombstone.DesiredNamespaceValue,
			digest:    digest,
			prior:     namespaceExpectedReference(tombstone.ExpectedPriorNamespace),
		}},
		commands: []terminalCommand{command},
	}

	expectObservationStart(mock, fixture.fenceSequence)
	expectObservationScans(mock, fixture.current, fixture.history, fixture.commands)
	mock.ExpectRollback()

	if _, err := authority.InspectNamespaceReservationLineage(context.Background()); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("synthetic tombstone observation = %v, want corrupt error", err)
	}
	assertExpectations(t, mock)
}

func TestInspectNamespaceReservationLineageJoinsRollbackFailure(t *testing.T) {
	authority, mock, closeDB := mockTenantAuthority(t)
	defer closeDB()
	tombstone := namespaceTombstone(t, testNamespace)
	command := completeNamespaceObservationCommand(t, testCommand2, tombstone, 2)
	digest := mustNamespaceDigest(t, tombstone.DesiredNamespaceValue)
	fixture := namespaceReservationObservationFixture{
		fenceSequence: 2,
		current: namespaceCurrent{
			Value:         tombstone.DesiredNamespaceValue,
			Digest:        digest,
			LastCommandID: testCommand2,
		},
		history: []namespaceReservationHistory{{
			commandID: testCommand2,
			sequence:  2,
			value:     tombstone.DesiredNamespaceValue,
			digest:    digest,
			prior:     namespaceExpectedReference(tombstone.ExpectedPriorNamespace),
		}},
		commands: []terminalCommand{command},
	}

	rollbackErr := errors.New("rollback failed")
	expectObservationStart(mock, fixture.fenceSequence)
	expectObservationScans(mock, fixture.current, fixture.history, fixture.commands)
	mock.ExpectRollback().WillReturnError(rollbackErr)

	_, err := authority.InspectNamespaceReservationLineage(context.Background())
	if !errors.Is(err, ErrCorrupt) || !errors.Is(err, rollbackErr) {
		t.Fatalf("inspection error = %v, want joined corruption and rollback errors", err)
	}
	assertExpectations(t, mock)
}

func TestInspectNamespaceReservationLineageRejectsInvalidDurableSequence(t *testing.T) {
	for _, test := range []struct {
		name          string
		rawSequence   string
		fenceSequence uint64
	}{
		{name: "zero", rawSequence: "0", fenceSequence: 1},
		{name: "noncanonical", rawSequence: "01", fenceSequence: 1},
		{name: "exceeds fence", rawSequence: "2", fenceSequence: 1},
	} {
		t.Run(test.name, func(t *testing.T) {
			authority, mock, closeDB := mockTenantAuthority(t)
			defer closeDB()
			fixture := activeReservationObservationFixture(t)
			fixture.fenceSequence = test.fenceSequence

			expectObservationStart(mock, fixture.fenceSequence)
			expectObservationCurrentScan(mock, fixture.current)
			expectObservationHistoryScanRawSequence(mock, fixture.history, test.rawSequence)
			if test.rawSequence == "2" {
				expectObservationAppliedCommandScan(mock, fixture.commands)
			}
			mock.ExpectRollback()

			if _, err := authority.InspectNamespaceReservationLineage(context.Background()); !errors.Is(err, ErrCorrupt) {
				t.Fatalf("observation = %v, want corrupt error", err)
			}
			assertExpectations(t, mock)
		})
	}
}

func TestInspectNamespaceReservationLineageRejectsCurrentAndTerminalDrift(t *testing.T) {
	t.Run("current head", func(t *testing.T) {
		authority, mock, closeDB := mockTenantAuthority(t)
		defer closeDB()
		fixture := activeReservationObservationFixture(t)
		fixture.current.LastCommandID = testCommand2

		expectObservationStart(mock, fixture.fenceSequence)
		expectObservationScans(mock, fixture.current, fixture.history, fixture.commands)
		mock.ExpectRollback()

		if _, err := authority.InspectNamespaceReservationLineage(context.Background()); !errors.Is(err, ErrCorrupt) {
			t.Fatalf("current-head observation = %v, want corrupt error", err)
		}
		assertExpectations(t, mock)
	})

	t.Run("terminal request", func(t *testing.T) {
		authority, mock, closeDB := mockTenantAuthority(t)
		defer closeDB()
		fixture := activeReservationObservationFixture(t)
		changed := namespaceReserve("com.example.other")
		fixture.commands[0] = completeNamespaceObservationCommand(
			t,
			testCommand1,
			changed,
			1,
		)
		fixture.commands[0].ResultingValue = ValueReference{
			Present: true,
			Digest:  fixture.history[0].digest,
		}
		fixture.commands[0].OutcomeDigest = ""
		fixture.commands[0] = mustCompleteCommand(t, fixture.commands[0])

		expectObservationStart(mock, fixture.fenceSequence)
		expectObservationScans(mock, fixture.current, fixture.history, fixture.commands)
		expectCommandRow(mock, fixture.commands[0])
		mock.ExpectRollback()

		if _, err := authority.InspectNamespaceReservationLineage(context.Background()); !errors.Is(err, ErrCorrupt) {
			t.Fatalf("terminal-drift observation = %v, want corrupt error", err)
		}
		assertExpectations(t, mock)
	})
}

func TestInspectNamespaceReservationLineageRejectsOrphanAppliedTerminal(t *testing.T) {
	authority, mock, closeDB := mockTenantAuthority(t)
	defer closeDB()
	fixture := activeReservationObservationFixture(t)
	orphan := fixture.commands[0]
	orphan.CommandID = testCommand2
	orphan.Sequence = 2
	orphan.OutcomeDigest = ""
	orphan = mustCompleteCommand(t, orphan)
	fixture.commands = append(fixture.commands, orphan)

	expectObservationStart(mock, 2)
	expectObservationScans(mock, fixture.current, fixture.history, fixture.commands)
	expectCommandRow(mock, fixture.commands[0])
	mock.ExpectRollback()

	if _, err := authority.InspectNamespaceReservationLineage(context.Background()); !errors.Is(err, ErrCorrupt) {
		t.Fatalf("orphan-terminal observation = %v, want corrupt error", err)
	}
	assertExpectations(t, mock)
}

type namespaceReservationObservationFixture struct {
	fenceSequence uint64
	current       namespaceCurrent
	history       []namespaceReservationHistory
	commands      []terminalCommand
}

func activeReservationObservationFixture(t *testing.T) namespaceReservationObservationFixture {
	t.Helper()
	request := namespaceReserve(testNamespace)
	digest := mustNamespaceDigest(t, request.DesiredNamespaceValue)
	command := completeNamespaceObservationCommand(t, testCommand1, request, 1)
	return namespaceReservationObservationFixture{
		fenceSequence: 1,
		current: namespaceCurrent{
			Value:         request.DesiredNamespaceValue,
			Digest:        digest,
			LastCommandID: testCommand1,
		},
		history: []namespaceReservationHistory{{
			commandID: testCommand1,
			sequence:  1,
			value:     request.DesiredNamespaceValue,
			digest:    digest,
		}},
		commands: []terminalCommand{command},
	}
}

func tombstonedReservationObservationFixture(t *testing.T) namespaceReservationObservationFixture {
	t.Helper()
	active := activeReservationObservationFixture(t)
	tombstone := namespaceTombstone(t, testNamespace)
	tombstoneDigest := mustNamespaceDigest(t, tombstone.DesiredNamespaceValue)
	tombstoneCommand := completeNamespaceObservationCommand(t, testCommand2, tombstone, 2)
	active.fenceSequence = 2
	active.current = namespaceCurrent{
		Value:         tombstone.DesiredNamespaceValue,
		Digest:        tombstoneDigest,
		LastCommandID: testCommand2,
	}
	active.history = append(active.history, namespaceReservationHistory{
		commandID: testCommand2,
		sequence:  2,
		value:     tombstone.DesiredNamespaceValue,
		digest:    tombstoneDigest,
		prior:     namespaceExpectedReference(tombstone.ExpectedPriorNamespace),
	})
	active.commands = append(active.commands, tombstoneCommand)
	return active
}

func completeNamespaceObservationCommand(
	t *testing.T,
	commandID tenantauthorityv1.CommandID,
	request tenantauthorityv1.TenantNamespaceCommandRequest,
	sequence uint64,
) terminalCommand {
	t.Helper()
	requestDigest, err := tenantauthorityv1.NamespaceCommandRequestDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	resultingDigest := mustNamespaceDigest(t, request.DesiredNamespaceValue)
	return mustCompleteCommand(t, terminalCommand{
		CommandID:      commandID,
		Kind:           commandKindNamespace,
		RequestDigest:  requestDigest,
		Result:         ResultApplied,
		ObservedPrior:  namespaceExpectedReference(request.ExpectedPriorNamespace),
		ResultingValue: ValueReference{Present: true, Digest: resultingDigest},
		Sequence:       sequence,
	})
}

func expectObservationStart(mock sqlmock.Sqlmock, fenceSequence uint64) {
	mock.ExpectBegin()
	mock.ExpectQuery(fenceLockQuery).WithArgs(authorityFenceKey).WillReturnRows(
		sqlmock.NewRows([]string{"FenceKey", "CommandSequence"}).
			AddRow(authorityFenceKey, []byte(uintText(fenceSequence))),
	)
}

func expectObservationScans(
	mock sqlmock.Sqlmock,
	current namespaceCurrent,
	history []namespaceReservationHistory,
	commands []terminalCommand,
) {
	expectObservationCurrentScan(mock, current)
	expectObservationHistoryScan(mock, history)
	expectObservationAppliedCommandScan(mock, commands)
}

func expectObservationCurrentScan(mock sqlmock.Sqlmock, current namespaceCurrent) {
	mock.ExpectQuery(namespaceReservationCurrentInventoryQuery).WillReturnRows(
		sqlmock.NewRows(namespaceCurrentColumns()).AddRow(
			[]byte(current.Value.TenantNamespaceID),
			current.Value.ValueVersion,
			string(current.Value.State),
			current.Digest,
			string(current.LastCommandID),
		),
	)
}

func expectObservationHistoryScan(
	mock sqlmock.Sqlmock,
	history []namespaceReservationHistory,
) {
	rows := sqlmock.NewRows([]string{
		"CommandId", "CommandSequence", "TenantNamespaceId", "ValueVersion",
		"State", "ValueDigest", "PriorValueDigest",
	})
	for _, item := range history {
		rows.AddRow(
			string(item.commandID),
			[]byte(uintText(item.sequence)),
			[]byte(item.value.TenantNamespaceID),
			item.value.ValueVersion,
			string(item.value.State),
			item.digest,
			nullableReference(item.prior),
		)
	}
	mock.ExpectQuery(namespaceReservationHistoryInventoryQuery).WillReturnRows(rows)
}

func expectObservationHistoryScanRawSequence(
	mock sqlmock.Sqlmock,
	history []namespaceReservationHistory,
	rawSequence string,
) {
	rows := sqlmock.NewRows([]string{
		"CommandId", "CommandSequence", "TenantNamespaceId", "ValueVersion",
		"State", "ValueDigest", "PriorValueDigest",
	})
	for index, item := range history {
		sequence := uintText(item.sequence)
		if index == 0 {
			sequence = rawSequence
		}
		rows.AddRow(
			string(item.commandID),
			[]byte(sequence),
			[]byte(item.value.TenantNamespaceID),
			item.value.ValueVersion,
			string(item.value.State),
			item.digest,
			nullableReference(item.prior),
		)
	}
	mock.ExpectQuery(namespaceReservationHistoryInventoryQuery).WillReturnRows(rows)
}

func expectObservationAppliedCommandScan(
	mock sqlmock.Sqlmock,
	commands []terminalCommand,
) {
	rows := sqlmock.NewRows([]string{"CommandId", "CommandSequence"})
	for _, command := range commands {
		rows.AddRow(string(command.CommandID), []byte(uintText(command.Sequence)))
	}
	mock.ExpectQuery(appliedNamespaceCommandInventoryQuery).
		WithArgs(string(commandKindNamespace), string(ResultApplied)).
		WillReturnRows(rows)
}
