package catalogtenant

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

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

var (
	activeOrgBindingNamespaceQuery = regexp.QuoteMeta(`SELECT TenantNamespaceId, ValueVersion, State,
ValueDigest, LastCommandId
FROM _System_CatalogTenantNamespaceReservation
WHERE TenantNamespaceId = ? FOR UPDATE`)
	activeOrgBindingCurrentQuery = regexp.QuoteMeta(`SELECT OrgId, TenantNamespaceId, ValueVersion,
State, ValueDigest, LastCommandId
FROM _System_CatalogTenantOrgNamespaceBinding WHERE OrgId = ? FOR UPDATE`)
	activeOrgBindingNamespaceHistoryQuery = regexp.QuoteMeta(`SELECT CommandId, CommandSequence,
TenantNamespaceId, ValueVersion, State, ValueDigest, PriorValueDigest
FROM _System_CatalogTenantNamespaceHistory
WHERE TenantNamespaceId = ? ORDER BY CommandSequence FOR UPDATE`)
	activeOrgBindingHistoryQuery = regexp.QuoteMeta(`SELECT CommandId, CommandSequence,
OrgId, TenantNamespaceId, ValueVersion, State, ValueDigest, PriorValueDigest
FROM _System_CatalogTenantOrgNamespaceBindingHistory
WHERE OrgId = ? ORDER BY CommandSequence FOR UPDATE`)
)

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

	expectActiveOrgBindingObservation(mock, fixture)
	mock.ExpectRollback()

	actual, err := authority.InspectActiveOrgBindingLineage(context.Background(), testOrg)
	if err != nil {
		t.Fatal(err)
	}
	want := ObservedActiveOrgBinding{OrgID: testOrg, TenantNamespaceID: testNamespace}
	if !reflect.DeepEqual(actual, want) {
		t.Fatalf("observed binding = %#v, want %#v", actual, want)
	}
	assertExpectations(t, mock)
}

func TestInspectActiveOrgBindingLineageRejectsInvalidOrInactiveCurrentFacts(t *testing.T) {
	for _, test := range []struct {
		name   string
		expect func(sqlmock.Sqlmock, activeOrgBindingObservation)
		want   error
	}{
		{
			name: "missing binding",
			expect: func(mock sqlmock.Sqlmock, _ activeOrgBindingObservation) {
				expectActiveOrgBindingStart(mock, 2)
				mock.ExpectQuery(activeOrgBindingCurrentQuery).WithArgs(testOrg).
					WillReturnRows(sqlmock.NewRows(bindingCurrentColumns()))
			},
			want: ErrNotFound,
		},
		{
			name: "tombstoned binding",
			expect: func(mock sqlmock.Sqlmock, fixture activeOrgBindingObservation) {
				expectActiveOrgBindingStart(mock, fixture.fenceSequence)
				fixture.binding.Value.State = tenantauthorityv1.OrgBindingStateTombstone
				fixture.binding.Digest = mustBindingDigest(t, fixture.binding.Value)
				expectActiveOrgBindingCurrent(mock, fixture.binding)
			},
			want: ErrConflict,
		},
		{
			name: "missing namespace",
			expect: func(mock sqlmock.Sqlmock, fixture activeOrgBindingObservation) {
				expectActiveOrgBindingStart(mock, fixture.fenceSequence)
				expectActiveOrgBindingCurrent(mock, fixture.binding)
				mock.ExpectQuery(activeOrgBindingNamespaceQuery).WithArgs(testNamespace).
					WillReturnRows(sqlmock.NewRows(namespaceCurrentColumns()))
			},
			want: ErrCorrupt,
		},
		{
			name: "tombstoned namespace",
			expect: func(mock sqlmock.Sqlmock, fixture activeOrgBindingObservation) {
				expectActiveOrgBindingStart(mock, fixture.fenceSequence)
				expectActiveOrgBindingCurrent(mock, fixture.binding)
				fixture.namespace.Value.State = tenantauthorityv1.NamespaceStateTombstone
				fixture.namespace.Digest = mustNamespaceDigest(t, fixture.namespace.Value)
				expectActiveOrgBindingNamespace(mock, fixture.namespace)
			},
			want: ErrConflict,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			authority, mock, closeDB := mockTenantAuthority(t)
			defer closeDB()
			fixture := activeOrgBindingObservationFixture(t)
			test.expect(mock, fixture)
			mock.ExpectRollback()

			if _, err := authority.InspectActiveOrgBindingLineage(context.Background(), testOrg); !errors.Is(err, test.want) {
				t.Fatalf("inspection = %v, want %v", err, test.want)
			}
			assertExpectations(t, mock)
		})
	}
}

func TestInspectActiveOrgBindingLineageRejectsLineageDrift(t *testing.T) {
	for _, test := range []struct {
		name   string
		mutate func(*activeOrgBindingObservation)
	}{
		{
			name: "namespace after binding",
			mutate: func(fixture *activeOrgBindingObservation) {
				fixture.fenceSequence = 3
				fixture.namespaceCommand.Sequence = 3
				fixture.namespaceCommand.OutcomeDigest = ""
				fixture.namespaceCommand = mustCompleteCommand(t, fixture.namespaceCommand)
			},
		},
		{
			name: "binding terminal namespace mismatch",
			mutate: func(fixture *activeOrgBindingObservation) {
				fixture.bindingCommand.ObservedNamespace = ValueReference{Present: true, Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
				fixture.bindingCommand.OutcomeDigest = ""
				fixture.bindingCommand = mustCompleteCommand(t, fixture.bindingCommand)
			},
		},
		{
			name: "multiple active binding history rows",
			mutate: func(fixture *activeOrgBindingObservation) {
				fixture.extraBindingHistory = true
			},
		},
		{
			name: "namespace sequence exceeds fence",
			mutate: func(fixture *activeOrgBindingObservation) {
				fixture.namespaceCommand.Sequence = 3
				fixture.namespaceCommand.OutcomeDigest = ""
				fixture.namespaceCommand = mustCompleteCommand(t, fixture.namespaceCommand)
			},
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			authority, mock, closeDB := mockTenantAuthority(t)
			defer closeDB()
			fixture := activeOrgBindingObservationFixture(t)
			test.mutate(&fixture)
			expectActiveOrgBindingUntilFailure(mock, fixture, test.name)
			mock.ExpectRollback()

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

func expectActiveOrgBindingUntilFailure(
	mock sqlmock.Sqlmock,
	fixture activeOrgBindingObservation,
	name string,
) {
	expectActiveOrgBindingStart(mock, fixture.fenceSequence)
	expectActiveOrgBindingCurrent(mock, fixture.binding)
	expectActiveOrgBindingNamespace(mock, fixture.namespace)
	expectActiveOrgBindingNamespaceHistory(mock, fixture)
	if name == "namespace sequence exceeds fence" {
		return
	}
	expectCommandRow(mock, fixture.namespaceCommand)
	expectActiveOrgBindingHistory(mock, fixture)
	if name == "multiple active binding history rows" {
		return
	}
	expectCommandRow(mock, fixture.bindingCommand)
}

func TestInspectActiveOrgBindingLineageJoinsRollbackFailure(t *testing.T) {
	authority, mock, closeDB := mockTenantAuthority(t)
	defer closeDB()
	fixture := activeOrgBindingObservationFixture(t)
	fixture.bindingCommand.ObservedNamespace = ValueReference{Present: true, Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
	fixture.bindingCommand.OutcomeDigest = ""
	fixture.bindingCommand = mustCompleteCommand(t, fixture.bindingCommand)
	rollbackErr := errors.New("rollback failed")

	expectActiveOrgBindingObservation(mock, fixture)
	mock.ExpectRollback().WillReturnError(rollbackErr)

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

type activeOrgBindingObservation struct {
	fenceSequence       uint64
	namespace           namespaceCurrent
	binding             bindingCurrent
	namespaceCommand    terminalCommand
	bindingCommand      terminalCommand
	extraBindingHistory bool
}

func activeOrgBindingObservationFixture(t *testing.T) activeOrgBindingObservation {
	t.Helper()
	namespaceRequest := namespaceReserve(testNamespace)
	bindingRequest := bindingBind(t, testOrg, testNamespace)
	return activeOrgBindingObservation{
		fenceSequence: 2,
		namespace: namespaceCurrent{
			Value:         namespaceRequest.DesiredNamespaceValue,
			Digest:        mustNamespaceDigest(t, namespaceRequest.DesiredNamespaceValue),
			LastCommandID: testCommand1,
		},
		binding: bindingCurrent{
			Value:         bindingRequest.DesiredBindingValue,
			Digest:        mustBindingDigest(t, bindingRequest.DesiredBindingValue),
			LastCommandID: testCommand2,
		},
		namespaceCommand: completeNamespaceObservationCommand(
			t,
			testCommand1,
			namespaceRequest,
			1,
		),
		bindingCommand: completeBindingObservationCommand(
			t,
			testCommand2,
			bindingRequest,
			2,
		),
	}
}

func completeBindingObservationCommand(
	t *testing.T,
	commandID tenantauthorityv1.CommandID,
	request tenantauthorityv1.TenantOrgNamespaceBindingCommandRequest,
	sequence uint64,
) terminalCommand {
	t.Helper()
	requestDigest, err := tenantauthorityv1.OrgBindingCommandRequestDigest(request)
	if err != nil {
		t.Fatal(err)
	}
	return mustCompleteCommand(t, terminalCommand{
		CommandID:         commandID,
		Kind:              commandKindBinding,
		RequestDigest:     requestDigest,
		Result:            ResultApplied,
		ObservedPrior:     bindingExpectedReference(request.ExpectedPriorBinding),
		ResultingValue:    ValueReference{Present: true, Digest: mustBindingDigest(t, request.DesiredBindingValue)},
		ObservedNamespace: ValueReference{Present: true, Digest: request.ExpectedNamespaceReservationDigest},
		Sequence:          sequence,
	})
}

func expectActiveOrgBindingObservation(
	mock sqlmock.Sqlmock,
	fixture activeOrgBindingObservation,
) {
	expectActiveOrgBindingStart(mock, fixture.fenceSequence)
	expectActiveOrgBindingCurrent(mock, fixture.binding)
	expectActiveOrgBindingNamespace(mock, fixture.namespace)
	expectActiveOrgBindingNamespaceHistory(mock, fixture)
	expectCommandRow(mock, fixture.namespaceCommand)
	expectActiveOrgBindingHistory(mock, fixture)
	expectCommandRow(mock, fixture.bindingCommand)
}

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

func expectActiveOrgBindingCurrent(mock sqlmock.Sqlmock, binding bindingCurrent) {
	mock.ExpectQuery(activeOrgBindingCurrentQuery).WithArgs(testOrg).WillReturnRows(
		sqlmock.NewRows(bindingCurrentColumns()).AddRow(
			[]byte(binding.Value.OrgID),
			[]byte(binding.Value.TenantNamespaceID),
			binding.Value.ValueVersion,
			string(binding.Value.State),
			binding.Digest,
			string(binding.LastCommandID),
		),
	)
}

func expectActiveOrgBindingNamespace(mock sqlmock.Sqlmock, namespace namespaceCurrent) {
	mock.ExpectQuery(activeOrgBindingNamespaceQuery).WithArgs(testNamespace).WillReturnRows(
		sqlmock.NewRows(namespaceCurrentColumns()).AddRow(
			[]byte(namespace.Value.TenantNamespaceID),
			namespace.Value.ValueVersion,
			string(namespace.Value.State),
			namespace.Digest,
			string(namespace.LastCommandID),
		),
	)
}

func expectActiveOrgBindingNamespaceHistory(
	mock sqlmock.Sqlmock,
	fixture activeOrgBindingObservation,
) {
	rows := sqlmock.NewRows([]string{
		"CommandId", "CommandSequence", "TenantNamespaceId", "ValueVersion",
		"State", "ValueDigest", "PriorValueDigest",
	}).AddRow(
		string(fixture.namespace.LastCommandID),
		[]byte(uintText(fixture.namespaceCommand.Sequence)),
		[]byte(fixture.namespace.Value.TenantNamespaceID),
		fixture.namespace.Value.ValueVersion,
		string(fixture.namespace.Value.State),
		fixture.namespace.Digest,
		nil,
	)
	mock.ExpectQuery(activeOrgBindingNamespaceHistoryQuery).WithArgs(testNamespace).
		WillReturnRows(rows)
}

func expectActiveOrgBindingHistory(
	mock sqlmock.Sqlmock,
	fixture activeOrgBindingObservation,
) {
	rows := sqlmock.NewRows([]string{
		"CommandId", "CommandSequence", "OrgId", "TenantNamespaceId", "ValueVersion",
		"State", "ValueDigest", "PriorValueDigest",
	}).AddRow(
		string(fixture.binding.LastCommandID),
		[]byte(uintText(fixture.bindingCommand.Sequence)),
		[]byte(fixture.binding.Value.OrgID),
		[]byte(fixture.binding.Value.TenantNamespaceID),
		fixture.binding.Value.ValueVersion,
		string(fixture.binding.Value.State),
		fixture.binding.Digest,
		nil,
	)
	if fixture.extraBindingHistory {
		rows.AddRow(
			string(fixture.binding.LastCommandID),
			[]byte("3"),
			[]byte(fixture.binding.Value.OrgID),
			[]byte(fixture.binding.Value.TenantNamespaceID),
			fixture.binding.Value.ValueVersion,
			string(fixture.binding.Value.State),
			fixture.binding.Digest,
			nil,
		)
	}
	mock.ExpectQuery(activeOrgBindingHistoryQuery).WithArgs(testOrg).WillReturnRows(rows)
}
