package catalogapp

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

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundlekeyv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundleslotv1"
)

const (
	testSlotAppID     = "io.example.app"
	testSlotNamespace = "io.example.app"
	testSlotRelease   = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
	testSlotKey       = layoutbundlekeyv1.LocalComponentKey("key256:1111111111111111111111111111111111111111111111111111111111111111")
)

var errSnapshotBeginProbe = errors.New("snapshot begin probe")

type snapshotBeginProbe struct {
	options *sql.TxOptions
}

func (p *snapshotBeginProbe) BeginTx(_ context.Context, options *sql.TxOptions) (*sql.Tx, error) {
	p.options = options
	if options != nil && options.ReadOnly {
		return nil, errors.New("database driver rejected read-only transaction option")
	}
	return nil, errSnapshotBeginProbe
}

func TestCatalogSnapshotReadDoesNotRequireDriverReadOnlyMode(t *testing.T) {
	probe := &snapshotBeginProbe{}
	if _, err := beginCatalogSnapshotRead(context.Background(), probe); !errors.Is(err, errSnapshotBeginProbe) {
		t.Fatalf("beginCatalogSnapshotRead error = %v, want probe error", err)
	}
	if probe.options != nil {
		t.Fatalf("beginCatalogSnapshotRead options = %+v, want database defaults", probe.options)
	}
}

func TestRegisterReleaseLayoutBundleSlotsInitialCreatesDurableOwnerAndBinding(t *testing.T) {
	declaration := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	expectMissingLayoutBundleSlot(mock, declaration)
	mock.ExpectExec(`INSERT INTO _System_CatalogAppLayoutBundleSlot\b`).
		WithArgs(declaration.AppID, declaration.LocalComponentKey, declaration.DeclarationComponentID,
			declaration.NamespaceID, "1", declaration.RevisionDigest).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec(`INSERT INTO _System_CatalogAppLayoutBundleSlotVersion\b`).
		WithArgs(declaration.AppID, declaration.LocalComponentKey, "1", declaration.RevisionDigest,
			declaration.TargetComponentID, nil).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec(`INSERT INTO _System_CatalogAppReleaseLayoutBundleSlot\b`).
		WithArgs(declaration.AppID, testSlotRelease, declaration.LocalComponentKey, "1").
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	if err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, declaration.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{declaration}); err != nil {
		t.Fatalf("register initial slot: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestRegisterReleaseLayoutBundleSlotsHistoricalReuseWritesOnlyReleaseBinding(t *testing.T) {
	initial := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	successor := testLayoutBundleSlotDeclaration(t, 2, "IssueV2", &initial.TargetComponentID)
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{initial, successor}, successor, true)
	mock.ExpectExec(`INSERT INTO _System_CatalogAppReleaseLayoutBundleSlot\b`).
		WithArgs(initial.AppID, testSlotRelease, initial.LocalComponentKey, "1").
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	if err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, initial.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{initial}); err != nil {
		t.Fatalf("reuse historical slot: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestRegisterReleaseLayoutBundleSlotsAppendUsesExactHeadCAS(t *testing.T) {
	initial := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	successor := testLayoutBundleSlotDeclaration(t, 2, "IssueV2", &initial.TargetComponentID)
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{initial}, initial, true)
	mock.ExpectExec(`(?s)UPDATE _System_CatalogAppLayoutBundleSlot.*HeadDefinitionVersion = \?, HeadRevisionDigest = \?.*WHERE AppId = \? AND LocalComponentKey = \? AND HeadDefinitionVersion = \? AND HeadRevisionDigest = \?`).
		WithArgs("2", successor.RevisionDigest, successor.AppID, successor.LocalComponentKey, "1", initial.RevisionDigest).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`INSERT INTO _System_CatalogAppLayoutBundleSlotVersion\b`).
		WithArgs(successor.AppID, successor.LocalComponentKey, "2", successor.RevisionDigest,
			successor.TargetComponentID, initial.TargetComponentID).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec(`INSERT INTO _System_CatalogAppReleaseLayoutBundleSlot\b`).
		WithArgs(successor.AppID, testSlotRelease, successor.LocalComponentKey, "2").
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	if err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, successor.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{successor}); err != nil {
		t.Fatalf("append successor slot: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestRegisterReleaseLayoutBundleSlotsCASMissConflictsAtomically(t *testing.T) {
	initial := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	successor := testLayoutBundleSlotDeclaration(t, 2, "IssueV2", &initial.TargetComponentID)
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{initial}, initial, true)
	mock.ExpectExec(`(?s)UPDATE _System_CatalogAppLayoutBundleSlot.*HeadDefinitionVersion = \?, HeadRevisionDigest = \?.*WHERE AppId = \? AND LocalComponentKey = \? AND HeadDefinitionVersion = \? AND HeadRevisionDigest = \?`).
		WithArgs("2", successor.RevisionDigest, successor.AppID, successor.LocalComponentKey, "1", initial.RevisionDigest).
		WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectRollback()

	err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, successor.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{successor})
	if !errors.Is(err, ErrConflict) {
		t.Fatalf("CAS miss error = %v, want ErrConflict", err)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestRegisterReleaseLayoutBundleSlotsRollsBackHeadCASWhenVersionInsertFails(t *testing.T) {
	initial := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	successor := testLayoutBundleSlotDeclaration(t, 2, "IssueV2", &initial.TargetComponentID)
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{initial}, initial, true)
	mock.ExpectExec(`(?s)UPDATE _System_CatalogAppLayoutBundleSlot.*HeadDefinitionVersion = \?, HeadRevisionDigest = \?.*WHERE AppId = \? AND LocalComponentKey = \? AND HeadDefinitionVersion = \? AND HeadRevisionDigest = \?`).
		WithArgs("2", successor.RevisionDigest, successor.AppID, successor.LocalComponentKey, "1", initial.RevisionDigest).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec(`INSERT INTO _System_CatalogAppLayoutBundleSlotVersion\b`).
		WithArgs(successor.AppID, successor.LocalComponentKey, "2", successor.RevisionDigest,
			successor.TargetComponentID, initial.TargetComponentID).
		WillReturnError(errors.New("injected immutable-version write failure"))
	mock.ExpectRollback()

	err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, successor.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{successor})
	if err == nil || !strings.Contains(err.Error(), "injected immutable-version write failure") {
		t.Fatalf("version write failure = %v", err)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestRegisterReleaseLayoutBundleSlotsMultiSlotConflictRollsBackEarlierWrites(t *testing.T) {
	first := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	second := testLayoutBundleSlotDeclaration(t, 2, "Other", &first.TargetComponentID)
	second.LocalComponentKey = layoutbundlekeyv1.LocalComponentKey("key256:2222222222222222222222222222222222222222222222222222222222222222")
	second.DeclarationComponentID = appauthority.ComponentID(second.NamespaceID, layoutbundleslotv1.ComponentKind, string(second.LocalComponentKey))
	second.RevisionDigest = testSlotDigest("2")
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	expectMissingLayoutBundleSlot(mock, first)
	mock.ExpectExec(`INSERT INTO _System_CatalogAppLayoutBundleSlot\b`).
		WithArgs(first.AppID, first.LocalComponentKey, first.DeclarationComponentID, first.NamespaceID, "1", first.RevisionDigest).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec(`INSERT INTO _System_CatalogAppLayoutBundleSlotVersion\b`).
		WithArgs(first.AppID, first.LocalComponentKey, "1", first.RevisionDigest, first.TargetComponentID, nil).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec(`INSERT INTO _System_CatalogAppReleaseLayoutBundleSlot\b`).
		WithArgs(first.AppID, testSlotRelease, first.LocalComponentKey, "1").
		WillReturnResult(sqlmock.NewResult(1, 1))
	expectMissingLayoutBundleSlot(mock, second)
	mock.ExpectRollback()

	err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, first.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{second, first})
	if !errors.Is(err, ErrConflict) {
		t.Fatalf("later slot gap error = %v, want ErrConflict", err)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestRegisterReleaseLayoutBundleSlotsRejectsDivergentHistoricalVersionWithoutWrites(t *testing.T) {
	initial := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	divergent := testLayoutBundleSlotDeclaration(t, 1, "IssueFork", nil)
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{initial}, initial, true)
	mock.ExpectRollback()

	err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, divergent.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{divergent})
	if !errors.Is(err, ErrConflict) {
		t.Fatalf("divergent definition error = %v, want ErrConflict", err)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestRegisterReleaseLayoutBundleSlotsOmissionPerformsNoSlotSQL(t *testing.T) {
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()
	mock.ExpectCommit()

	if err := registerReleaseLayoutBundleSlotsTx(context.Background(), tx, testSlotAppID, testSlotRelease, []layoutbundleslotv1.Declaration{}); err != nil {
		t.Fatalf("register omitted slots: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestVerifyReleaseLayoutBundleSlotsRequiresExactCompleteStoredSet(t *testing.T) {
	declaration := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)

	t.Run("exact", func(t *testing.T) {
		db, mock, tx := newLayoutBundleSlotTx(t)
		defer db.Close()
		expectReleaseLayoutBundleSlotBindings(mock, []layoutbundleslotv1.Declaration{declaration})
		expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{declaration}, declaration, true)
		mock.ExpectRollback()
		if err := verifyReleaseLayoutBundleSlotsTx(context.Background(), tx, declaration.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{declaration}); err != nil {
			t.Fatalf("verify exact retry: %v", err)
		}
		if err := tx.Rollback(); err != nil {
			t.Fatal(err)
		}
		assertLayoutBundleSlotExpectations(t, mock)
	})

	t.Run("mismatch", func(t *testing.T) {
		db, mock, tx := newLayoutBundleSlotTx(t)
		defer db.Close()
		stored := declaration
		stored.RevisionDigest = testSlotDigest("e")
		expectReleaseLayoutBundleSlotBindings(mock, []layoutbundleslotv1.Declaration{stored})
		mock.ExpectRollback()
		err := verifyReleaseLayoutBundleSlotsTx(context.Background(), tx, declaration.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{declaration})
		if !errors.Is(err, ErrCorrupt) {
			t.Fatalf("mismatched retry error = %v, want ErrCorrupt", err)
		}
		if err := tx.Rollback(); err != nil {
			t.Fatal(err)
		}
		assertLayoutBundleSlotExpectations(t, mock)
	})

	t.Run("missing", func(t *testing.T) {
		db, mock, tx := newLayoutBundleSlotTx(t)
		defer db.Close()
		expectReleaseLayoutBundleSlotBindings(mock, nil)
		mock.ExpectRollback()
		err := verifyReleaseLayoutBundleSlotsTx(context.Background(), tx, declaration.AppID, testSlotRelease, []layoutbundleslotv1.Declaration{declaration})
		if !errors.Is(err, ErrCorrupt) {
			t.Fatalf("incomplete retry error = %v, want ErrCorrupt", err)
		}
		if err := tx.Rollback(); err != nil {
			t.Fatal(err)
		}
		assertLayoutBundleSlotExpectations(t, mock)
	})
}

func TestReadLayoutBundleSlotLineageRejectsCorruptHistoryAndHead(t *testing.T) {
	initial := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	successor := testLayoutBundleSlotDeclaration(t, 2, "IssueV2", &initial.TargetComponentID)

	t.Run("history gap", func(t *testing.T) {
		db, mock, tx := newLayoutBundleSlotTx(t)
		defer db.Close()
		expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{successor}, successor, false)
		mock.ExpectRollback()
		_, err := readLayoutBundleSlotLineageTx(context.Background(), tx, initial.AppID, initial.LocalComponentKey, false)
		if !errors.Is(err, ErrCorrupt) {
			t.Fatalf("history corruption error = %v, want ErrCorrupt", err)
		}
		if err := tx.Rollback(); err != nil {
			t.Fatal(err)
		}
		assertLayoutBundleSlotExpectations(t, mock)
	})

	t.Run("head mismatch", func(t *testing.T) {
		db, mock, tx := newLayoutBundleSlotTx(t)
		defer db.Close()
		corruptHead := initial
		corruptHead.RevisionDigest = testSlotDigest("f")
		expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{initial}, corruptHead, false)
		mock.ExpectRollback()
		_, err := readLayoutBundleSlotLineageTx(context.Background(), tx, initial.AppID, initial.LocalComponentKey, false)
		if !errors.Is(err, ErrCorrupt) {
			t.Fatalf("head corruption error = %v, want ErrCorrupt", err)
		}
		if err := tx.Rollback(); err != nil {
			t.Fatal(err)
		}
		assertLayoutBundleSlotExpectations(t, mock)
	})
}

func TestReadReleaseLayoutBundleSlotsRejectsMissingDurableOwner(t *testing.T) {
	declaration := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	db, mock, tx := newLayoutBundleSlotTx(t)
	defer db.Close()

	mock.ExpectQuery(`(?s)FROM _System_CatalogAppReleaseLayoutBundleSlot b.*LEFT JOIN _System_CatalogAppLayoutBundleSlot s.*LEFT JOIN _System_CatalogAppLayoutBundleSlotVersion v`).
		WithArgs(declaration.AppID, testSlotRelease).
		WillReturnRows(sqlmock.NewRows(releaseLayoutBundleSlotColumns()).AddRow(
			declaration.AppID, testSlotRelease, declaration.LocalComponentKey, "1", nil, nil, nil, nil, nil,
		))
	mock.ExpectRollback()

	_, err := readReleaseLayoutBundleSlotsTx(context.Background(), tx, declaration.AppID, testSlotRelease)
	if !errors.Is(err, ErrCorrupt) {
		t.Fatalf("missing durable owner error = %v, want ErrCorrupt", err)
	}
	if err := tx.Rollback(); err != nil {
		t.Fatal(err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestReadReleaseLayoutBundleSlotsValidatesBindingAgainstCompleteLineage(t *testing.T) {
	declaration := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}

	mock.ExpectBegin()
	expectLayoutBundleSlotReleaseIdentity(mock, declaration.AppID, testSlotRelease)
	expectReleaseLayoutBundleSlotBindings(mock, []layoutbundleslotv1.Declaration{declaration})
	expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{declaration}, declaration, false)
	mock.ExpectCommit()

	bindings, err := authority.ReadReleaseLayoutBundleSlots(context.Background(), declaration.AppID, testSlotRelease)
	if err != nil {
		t.Fatalf("read release slot bindings: %v", err)
	}
	if len(bindings) != 1 {
		t.Fatalf("binding count = %d, want 1", len(bindings))
	}
	binding := bindings[0]
	if binding.AppID != declaration.AppID || binding.AppReleaseDigest != testSlotRelease ||
		binding.LocalComponentKey != declaration.LocalComponentKey || binding.DefinitionVersion != declaration.DefinitionVersion ||
		binding.RevisionDigest != declaration.RevisionDigest || binding.TargetComponentID != declaration.TargetComponentID {
		t.Fatalf("unexpected release slot binding: %#v", binding)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestReadReleaseLayoutBundleSlotsPropagatesCorruptLineage(t *testing.T) {
	declaration := testLayoutBundleSlotDeclaration(t, 1, "Issue", nil)
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	authority, err := New(db, Options{})
	if err != nil {
		t.Fatal(err)
	}

	corruptHead := declaration
	corruptHead.RevisionDigest = testSlotDigest("f")
	mock.ExpectBegin()
	expectLayoutBundleSlotReleaseIdentity(mock, declaration.AppID, testSlotRelease)
	expectReleaseLayoutBundleSlotBindings(mock, []layoutbundleslotv1.Declaration{declaration})
	expectLayoutBundleSlotLineage(mock, []layoutbundleslotv1.Declaration{declaration}, corruptHead, false)
	mock.ExpectRollback()

	_, err = authority.ReadReleaseLayoutBundleSlots(context.Background(), declaration.AppID, testSlotRelease)
	if !errors.Is(err, ErrCorrupt) {
		t.Fatalf("corrupt release slot lineage error = %v, want ErrCorrupt", err)
	}
	assertLayoutBundleSlotExpectations(t, mock)
}

func TestParseSlotDefinitionVersionRequiresCanonicalPositiveUint64(t *testing.T) {
	for _, raw := range []string{"", "0", "01", "+1", "-1", "1_0", "18446744073709551616"} {
		if value, err := parseSlotDefinitionVersion([]byte(raw)); err == nil {
			t.Fatalf("parseSlotDefinitionVersion(%q) = %d, want rejection", raw, value)
		}
	}
	for raw, expected := range map[string]uint64{"1": 1, "18446744073709551615": ^uint64(0)} {
		if value, err := parseSlotDefinitionVersion([]byte(raw)); err != nil || value != expected {
			t.Fatalf("parseSlotDefinitionVersion(%q) = %d, %v; want %d", raw, value, err, expected)
		}
	}
}

func newLayoutBundleSlotTx(t *testing.T) (*sql.DB, sqlmock.Sqlmock, *sql.Tx) {
	t.Helper()
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	mock.ExpectBegin()
	tx, err := db.BeginTx(context.Background(), nil)
	if err != nil {
		db.Close()
		t.Fatal(err)
	}
	return db, mock, tx
}

func expectMissingLayoutBundleSlot(mock sqlmock.Sqlmock, declaration layoutbundleslotv1.Declaration) {
	mock.ExpectQuery(`(?s)SELECT AppId, LocalComponentKey, DeclarationComponentId, NamespaceId, HeadDefinitionVersion, HeadRevisionDigest.*FROM _System_CatalogAppLayoutBundleSlot.*FOR UPDATE`).
		WithArgs(declaration.AppID, declaration.LocalComponentKey).
		WillReturnRows(sqlmock.NewRows(layoutBundleSlotReservationColumns()))
}

func expectLayoutBundleSlotLineage(mock sqlmock.Sqlmock, revisions []layoutbundleslotv1.Declaration, head layoutbundleslotv1.Declaration, forUpdate bool) {
	reservationQuery := `(?s)SELECT AppId, LocalComponentKey, DeclarationComponentId, NamespaceId, HeadDefinitionVersion, HeadRevisionDigest.*FROM _System_CatalogAppLayoutBundleSlot`
	versionQuery := `(?s)SELECT AppId, LocalComponentKey, DefinitionVersion, RevisionDigest, TargetComponentId, PreviousTargetComponentId.*FROM _System_CatalogAppLayoutBundleSlotVersion`
	if forUpdate {
		reservationQuery += `.*FOR UPDATE`
		versionQuery += `.*FOR UPDATE`
	}
	mock.ExpectQuery(reservationQuery).
		WithArgs(head.AppID, head.LocalComponentKey).
		WillReturnRows(sqlmock.NewRows(layoutBundleSlotReservationColumns()).
			AddRow(head.AppID, head.LocalComponentKey, head.DeclarationComponentID, head.NamespaceID,
				strconv.FormatUint(head.DefinitionVersion, 10), head.RevisionDigest))
	rows := sqlmock.NewRows([]string{"AppId", "LocalComponentKey", "DefinitionVersion", "RevisionDigest", "TargetComponentId", "PreviousTargetComponentId"})
	for _, revision := range revisions {
		var previous any
		if revision.PreviousTargetComponentID != nil {
			previous = *revision.PreviousTargetComponentID
		}
		rows.AddRow(revision.AppID, revision.LocalComponentKey, strconv.FormatUint(revision.DefinitionVersion, 10),
			revision.RevisionDigest, revision.TargetComponentID, previous)
	}
	mock.ExpectQuery(versionQuery).WithArgs(head.AppID, head.LocalComponentKey).WillReturnRows(rows)
}

func expectReleaseLayoutBundleSlotBindings(mock sqlmock.Sqlmock, declarations []layoutbundleslotv1.Declaration) {
	rows := sqlmock.NewRows(releaseLayoutBundleSlotColumns())
	for _, declaration := range declarations {
		var previous any
		if declaration.PreviousTargetComponentID != nil {
			previous = *declaration.PreviousTargetComponentID
		}
		rows.AddRow(declaration.AppID, testSlotRelease, declaration.LocalComponentKey, strconv.FormatUint(declaration.DefinitionVersion, 10),
			declaration.DeclarationComponentID, declaration.NamespaceID, declaration.RevisionDigest,
			declaration.TargetComponentID, previous)
	}
	mock.ExpectQuery(`(?s)FROM _System_CatalogAppReleaseLayoutBundleSlot b.*LEFT JOIN _System_CatalogAppLayoutBundleSlot s.*LEFT JOIN _System_CatalogAppLayoutBundleSlotVersion v`).
		WithArgs(testSlotAppID, testSlotRelease).
		WillReturnRows(rows)
}

func expectLayoutBundleSlotReleaseIdentity(mock sqlmock.Sqlmock, appID, appReleaseDigest string) {
	mock.ExpectQuery(`(?s)SELECT AppId, AppReleaseDigest FROM _System_CatalogAppRelease.*WHERE AppId = \? AND AppReleaseDigest = \?`).
		WithArgs(appID, appReleaseDigest).
		WillReturnRows(sqlmock.NewRows([]string{"AppId", "AppReleaseDigest"}).AddRow(appID, appReleaseDigest))
}

func releaseLayoutBundleSlotColumns() []string {
	return []string{"AppId", "AppReleaseDigest", "LocalComponentKey", "DefinitionVersion", "DeclarationComponentId", "NamespaceId", "RevisionDigest", "TargetComponentId", "PreviousTargetComponentId"}
}

func layoutBundleSlotReservationColumns() []string {
	return []string{"AppId", "LocalComponentKey", "DeclarationComponentId", "NamespaceId", "HeadDefinitionVersion", "HeadRevisionDigest"}
}

func assertLayoutBundleSlotExpectations(t *testing.T, mock sqlmock.Sqlmock) {
	t.Helper()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

func testLayoutBundleSlotDeclaration(t *testing.T, definitionVersion uint64, targetBusinessKey string, previousTarget *string) layoutbundleslotv1.Declaration {
	t.Helper()
	targetBehavior := []byte(`{"fields":["Name"]}`)
	targetDigest, err := appauthority.ComponentDigest("metadata.object.v1", 1, targetBehavior)
	if err != nil {
		t.Fatal(err)
	}
	target := appauthority.Component{
		ComponentKind: "metadata.object.v1", ComponentFormatVersion: 1,
		NamespaceID: testSlotNamespace, BusinessKey: targetBusinessKey, DefinitionVersion: "1",
		LocalDependencies: []string{}, ExternalComponentRefs: []appauthority.ExternalComponentRef{},
		OverridePolicy: appauthority.OverridePolicyReplace,
		RemovalPolicy:  appauthority.RemovalPolicyBlockIfReferencedOrOverridden,
		ContentDigest:  targetDigest,
	}
	target.ComponentID = appauthority.ComponentID(target.NamespaceID, target.ComponentKind, target.BusinessKey)
	behavior, err := layoutbundleslotv1.CanonicalBehaviorBytes(layoutbundleslotv1.Behavior{
		SchemaVersion: layoutbundleslotv1.BehaviorSchemaVersion, LocalComponentKey: testSlotKey,
		TargetComponentID: target.ComponentID, PreviousTargetComponentID: previousTarget,
	})
	if err != nil {
		t.Fatal(err)
	}
	slotDigest, err := appauthority.ComponentDigest(layoutbundleslotv1.ComponentKind, layoutbundleslotv1.ComponentFormatVersion, behavior)
	if err != nil {
		t.Fatal(err)
	}
	slot := appauthority.Component{
		ComponentKind: layoutbundleslotv1.ComponentKind, ComponentFormatVersion: layoutbundleslotv1.ComponentFormatVersion,
		NamespaceID: testSlotNamespace, BusinessKey: string(testSlotKey), DefinitionVersion: strconv.FormatUint(definitionVersion, 10),
		LocalDependencies: []string{target.ComponentID}, ExternalComponentRefs: []appauthority.ExternalComponentRef{},
		OverridePolicy: appauthority.OverridePolicyReplace,
		RemovalPolicy:  appauthority.RemovalPolicyBlockIfReferencedOrOverridden,
		ContentDigest:  slotDigest,
	}
	slot.ComponentID = appauthority.ComponentID(slot.NamespaceID, slot.ComponentKind, slot.BusinessKey)
	type componentAndBehavior struct {
		component appauthority.Component
		behavior  []byte
	}
	components := []componentAndBehavior{{component: target, behavior: targetBehavior}, {component: slot, behavior: behavior}}
	sort.Slice(components, func(left, right int) bool {
		return components[left].component.ComponentID < components[right].component.ComponentID
	})
	bundle := appauthority.MetadataBundle{SchemaVersion: appauthority.MetadataBundleSchemaVersion, Components: make([]appauthority.Component, len(components))}
	blobs := make([]appauthority.ComponentBlob, len(components))
	for index, value := range components {
		bundle.Components[index] = value.component
		blobs[index] = appauthority.ComponentBlob{ComponentID: value.component.ComponentID, CanonicalBehavior: value.behavior}
	}
	manifest := appauthority.AppManifest{
		SchemaVersion: appauthority.AppManifestSchemaVersion,
		AppID:         testSlotAppID, PublisherID: "io.example", SemanticVersion: "1.0.0",
		MetadataBundleDigest: testSlotDigest("0"),
		Dependencies:         []appauthority.Dependency{}, OwnedNamespaces: []string{testSlotNamespace},
		RequiredProtocols: []appauthority.ProtocolRequirement{}, RequiredEngineCapabilities: []string{},
		ProvidedCapabilities:  []appauthority.ProvidedCapability{},
		FrontendCompatibility: appauthority.FrontendCompatibility{Mode: appauthority.FrontendModeNone},
	}
	manifest.MetadataBundleDigest, err = appauthority.MetadataBundleDigest(bundle, manifest)
	if err != nil {
		t.Fatal(err)
	}
	_, declarations, err := layoutbundleslotv1.VerifyPackageDeclarations(appauthority.Package{Manifest: manifest, Bundle: bundle, ComponentBlobs: blobs})
	if err != nil {
		t.Fatal(err)
	}
	if len(declarations) != 1 {
		t.Fatalf("slot declaration count = %d", len(declarations))
	}
	return declarations[0]
}

func testSlotDigest(character string) string {
	return "sha256:" + strings.Repeat(character, 64)
}
