package layoutownership

import (
	"context"
	"database/sql"
	"encoding/json"
	"strings"
	"testing"

	sqlmock "github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/shared/pkg/models"
)

// TestUpsertManagedPageLayoutRoundTripsRecordStates verifies that a PageLayout
// with RecordStates survives a JSON marshal → DB write → JSON unmarshal cycle
// with byte-identical RecordStates output. The upsert path uses json.Marshal;
// every controlplane PageLayout read uses DecodePageLayoutConfig.
func TestUpsertManagedPageLayoutRoundTripsRecordStates(t *testing.T) {
	rules := []models.RecordStateRule{
		{
			ID:       "state-closed",
			When:     "record.Status == 'Closed'",
			Priority: 20,
			Banner: &models.RecordStateBanner{
				Severity: "info",
				Title:    "Closed",
				Body:     "This record is closed.",
				LinkIDs:  []string{"link-account"},
			},
			Links: []models.RecordStateLink{
				{
					ID:                    "link-account",
					Label:                 "View Account",
					ReferenceFieldAPIName: "AccountId",
					ObjectAPIName:         "Account",
					Placement:             "quick-links",
				},
			},
			ActionOverrides: []models.RecordStateActionOverride{
				{ActionKey: "host:edit-record", Mode: "hide"},
				{ActionKey: "standard-action:Convert", Mode: "disable", Reason: "Record is closed"},
			},
			ReadOnly: &models.RecordStateReadOnly{Enabled: true, Reason: "Closed records are read-only"},
		},
	}

	layout := models.PageLayout{
		ID:            "layout-round-trip-test",
		ObjectAPIName: "Widget",
		LayoutName:    "Widget Layout",
		RecordStates:  rules,
		Sections: []models.PageSection{
			{ID: "s1", Label: "Details", Fields: []string{"Name"}},
		},
	}

	// Simulate the UpsertManagedPageLayout JSON marshal path.
	configJSON, err := json.Marshal(layout)
	if err != nil {
		t.Fatalf("json.Marshal: %v", err)
	}

	// Simulate the controlplane lossless PageLayout decode path.
	restored, err := DecodePageLayoutConfig(configJSON)
	if err != nil {
		t.Fatalf("DecodePageLayoutConfig: %v", err)
	}

	if len(restored.RecordStates) != 1 {
		t.Fatalf("RecordStates len = %d, want 1", len(restored.RecordStates))
	}
	got := restored.RecordStates[0]

	if got.ID != rules[0].ID {
		t.Errorf("ID = %q, want %q", got.ID, rules[0].ID)
	}
	if got.When != rules[0].When {
		t.Errorf("When = %q, want %q", got.When, rules[0].When)
	}
	if got.Priority != rules[0].Priority {
		t.Errorf("Priority = %d, want %d", got.Priority, rules[0].Priority)
	}

	// Banner
	if got.Banner == nil {
		t.Fatal("Banner is nil after round-trip")
	}
	if got.Banner.Severity != "info" {
		t.Errorf("Banner.Severity = %q, want info", got.Banner.Severity)
	}
	if got.Banner.Title != "Closed" {
		t.Errorf("Banner.Title = %q, want Closed", got.Banner.Title)
	}
	if len(got.Banner.LinkIDs) != 1 || got.Banner.LinkIDs[0] != "link-account" {
		t.Errorf("Banner.LinkIDs = %v, want [link-account]", got.Banner.LinkIDs)
	}

	// Links
	if len(got.Links) != 1 {
		t.Fatalf("Links len = %d, want 1", len(got.Links))
	}
	if got.Links[0].ReferenceFieldAPIName != "AccountId" {
		t.Errorf("Links[0].ReferenceFieldApiName = %q, want AccountId", got.Links[0].ReferenceFieldAPIName)
	}
	if got.Links[0].ObjectAPIName != "Account" {
		t.Errorf("Links[0].ObjectApiName = %q, want Account", got.Links[0].ObjectAPIName)
	}
	if got.Links[0].Placement != "quick-links" {
		t.Errorf("Links[0].Placement = %q, want quick-links", got.Links[0].Placement)
	}

	// ActionOverrides
	if len(got.ActionOverrides) != 2 {
		t.Fatalf("ActionOverrides len = %d, want 2", len(got.ActionOverrides))
	}
	if got.ActionOverrides[0].ActionKey != "host:edit-record" || got.ActionOverrides[0].Mode != "hide" {
		t.Errorf("ActionOverrides[0] = %+v, want {host:edit-record hide}", got.ActionOverrides[0])
	}
	if got.ActionOverrides[1].ActionKey != "standard-action:Convert" || got.ActionOverrides[1].Mode != "disable" {
		t.Errorf("ActionOverrides[1] = %+v, want {standard-action:Convert disable}", got.ActionOverrides[1])
	}

	// ReadOnly
	if got.ReadOnly == nil {
		t.Fatal("ReadOnly is nil after round-trip")
	}
	if !got.ReadOnly.Enabled {
		t.Error("ReadOnly.Enabled want true after round-trip")
	}
	if got.ReadOnly.Reason != "Closed records are read-only" {
		t.Errorf("ReadOnly.Reason = %q, want 'Closed records are read-only'", got.ReadOnly.Reason)
	}

	// Re-marshal and compare canonical JSON — ensures byte-identical round-trip.
	remarshal, err := json.Marshal(restored)
	if err != nil {
		t.Fatalf("re-marshal: %v", err)
	}
	if string(configJSON) != string(remarshal) {
		t.Errorf("canonical JSON changed across round-trip:\nwant: %s\n got: %s", configJSON, remarshal)
	}
}

// TestUpsertManagedPageLayoutCallsDB verifies that UpsertManagedPageLayout
// executes an INSERT ... ON DUPLICATE KEY UPDATE statement against the DB.
func TestUpsertManagedPageLayoutCallsDB(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "layout-upsert-test",
		ObjectAPIName: "Widget",
		LayoutName:    "Widget Layout",
		RecordStates: []models.RecordStateRule{
			{ID: "state-active", When: "record.Active == true"},
		},
	}

	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectExec(`INSERT INTO _System_PageLayout`).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := UpsertManagedPageLayout(context.Background(), db, layout, "curated", "salesforce-crm-standard"); err != nil {
		t.Fatalf("UpsertManagedPageLayout() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutDuplicateKeyRepairsBusinessKeyColumns(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
		IsDefault:     true,
	}

	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectExec(`INSERT INTO _System_PageLayout[\s\S]*ON DUPLICATE KEY UPDATE[\s\S]*ObjectApiName[\s]*=[\s]*VALUES\(ObjectApiName\),[\s\S]*LayoutName[\s]*=[\s]*VALUES\(LayoutName\)`).
		WithArgs(
			layout.ID, layout.ObjectAPIName, layout.LayoutName, layout.Type,
			layout.IsDefault,
			sqlmock.AnyArg(),
			"curated",
			"salesforce-crm-standard",
		).
		WillReturnResult(sqlmock.NewResult(0, 1))

	if err := UpsertManagedPageLayout(context.Background(), db, layout, "curated", "salesforce-crm-standard"); err != nil {
		t.Fatalf("UpsertManagedPageLayout() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutTxDuplicateKeyRepairsBusinessKeyColumns(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
		IsDefault:     true,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectExec(`INSERT INTO _System_PageLayout[\s\S]*ON DUPLICATE KEY UPDATE[\s\S]*ObjectApiName[\s]*=[\s]*VALUES\(ObjectApiName\),[\s\S]*LayoutName[\s]*=[\s]*VALUES\(LayoutName\)`).
		WithArgs(
			layout.ID, layout.ObjectAPIName, layout.LayoutName, layout.Type,
			layout.IsDefault,
			sqlmock.AnyArg(),
			"curated",
			"salesforce-crm-standard",
		).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("Begin() error = %v", err)
	}
	if err := upsertManagedPageLayoutTx(context.Background(), tx, layout, "curated", "salesforce-crm-standard"); err != nil {
		t.Fatalf("upsertManagedPageLayoutTx() error = %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatalf("Commit() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutRejectsAdminOwnedRowCollision(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
	}

	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow(layout.ID, `{"layout":"admin"}`, "admin_created", false, true, ""))

	err = UpsertManagedPageLayout(context.Background(), db, layout, "curated", "salesforce-crm-standard")
	if err == nil {
		t.Fatal("UpsertManagedPageLayout() error = nil, want admin-owned collision refusal")
	}
	if !strings.Contains(err.Error(), "collides with admin-owned row") {
		t.Fatalf("error = %v, want admin-owned collision label", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutTxRejectsAdminOwnedRowCollision(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow(layout.ID, `{"layout":"admin"}`, "admin_created", false, true, ""))
	mock.ExpectRollback()

	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("Begin() error = %v", err)
	}
	err = upsertManagedPageLayoutTx(context.Background(), tx, layout, "curated", "salesforce-crm-standard")
	if err == nil {
		t.Fatal("upsertManagedPageLayoutTx() error = nil, want admin-owned collision refusal")
	}
	if !strings.Contains(err.Error(), "collides with admin-owned row") {
		t.Fatalf("error = %v, want admin-owned collision label", err)
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutRejectsDifferentOwnerPackCollision(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
	}

	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow(layout.ID, `{"layout":"managed"}`, "curated", false, true, "pack-a"))

	err = UpsertManagedPageLayout(context.Background(), db, layout, "curated", "pack-b")
	if err == nil {
		t.Fatal("UpsertManagedPageLayout() error = nil, want owner-pack collision refusal")
	}
	if !strings.Contains(err.Error(), "collides with owner pack") {
		t.Fatalf("error = %v, want owner-pack collision label", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutTxRejectsDifferentOwnerPackCollision(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow(layout.ID, `{"layout":"managed"}`, "curated", false, true, "pack-a"))
	mock.ExpectRollback()

	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("Begin() error = %v", err)
	}
	err = upsertManagedPageLayoutTx(context.Background(), tx, layout, "curated", "pack-b")
	if err == nil {
		t.Fatal("upsertManagedPageLayoutTx() error = nil, want owner-pack collision refusal")
	}
	if !strings.Contains(err.Error(), "collides with owner pack") {
		t.Fatalf("error = %v, want owner-pack collision label", err)
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutRejectsMalformedManagedRowCollision(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
	}

	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow(layout.ID, `{"layout":"managed"}`, "curated", false, true, ""))

	err = UpsertManagedPageLayout(context.Background(), db, layout, "curated", "pack-b")
	if err == nil {
		t.Fatal("UpsertManagedPageLayout() error = nil, want malformed managed-row collision refusal")
	}
	if !strings.Contains(err.Error(), "malformed managed row") {
		t.Fatalf("error = %v, want malformed managed-row label", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutRejectsSamePackBusinessKeyCollisionWithDifferentRowID(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default_new",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
		IsDefault:     true,
	}

	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("curated_Quote_default_existing", `{"layout":"managed"}`, "curated", false, true, "pack-a"))

	err = UpsertManagedPageLayout(context.Background(), db, layout, "curated", "pack-a")
	if err == nil {
		t.Fatal("UpsertManagedPageLayout() error = nil, want same-pack business-key collision refusal")
	}
	if !strings.Contains(err.Error(), "collides with existing row") {
		t.Fatalf("error = %v, want same-pack business-key collision label", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutAdoptsSamePackBusinessKeyCollisionForNonDefault(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "managed_Quote_alt",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Alternate Layout",
		IsDefault:     false,
	}

	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("managed_Quote_alt_existing", `{"layout":"managed"}`, "curated", false, false, "pack-a"))
	mock.ExpectExec("INSERT INTO _System_PageLayout").
		WillReturnResult(sqlmock.NewResult(1, 1))

	err = UpsertManagedPageLayout(context.Background(), db, layout, "curated", "pack-a")
	if err != nil {
		t.Fatalf("UpsertManagedPageLayout() unexpected error: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutTxRejectsMalformedManagedRowCollision(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow(layout.ID, `{"layout":"managed"}`, "curated", false, true, ""))
	mock.ExpectRollback()

	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("Begin() error = %v", err)
	}
	err = upsertManagedPageLayoutTx(context.Background(), tx, layout, "curated", "pack-b")
	if err == nil {
		t.Fatal("upsertManagedPageLayoutTx() error = nil, want malformed managed-row collision refusal")
	}
	if !strings.Contains(err.Error(), "malformed managed row") {
		t.Fatalf("error = %v, want malformed managed-row label", err)
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutTxRejectsSamePackBusinessKeyCollisionWithDifferentRowID(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Quote_default_new",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Layout",
		IsDefault:     true,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("curated_Quote_default_existing", `{"layout":"managed"}`, "curated", false, true, "pack-a"))

	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("Begin() error = %v", err)
	}
	err = upsertManagedPageLayoutTx(context.Background(), tx, layout, "curated", "pack-a")
	if err == nil {
		t.Fatal("upsertManagedPageLayoutTx() error = nil, want same-pack business-key collision refusal")
	}
	if !strings.Contains(err.Error(), "collides with existing row") {
		t.Fatalf("error = %v, want same-pack business-key collision label", err)
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutTxAdoptsSamePackBusinessKeyCollisionForNonDefault(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "managed_Quote_alt",
		ObjectAPIName: "Quote",
		LayoutName:    "Quote Alternate Layout",
		IsDefault:     false,
	}

	mock.ExpectBegin()
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("managed_Quote_alt_existing", `{"layout":"managed"}`, "curated", false, false, "pack-a"))
	mock.ExpectExec("INSERT INTO _System_PageLayout").
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("Begin() error = %v", err)
	}
	err = upsertManagedPageLayoutTx(context.Background(), tx, layout, "curated", "pack-a")
	if err != nil {
		t.Fatalf("upsertManagedPageLayoutTx() unexpected error: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatalf("Commit() error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

// TestUpsertManagedPageLayoutSetsOwnerPackForSourceBacked asserts that
// UpsertManagedPageLayout passes a non-NULL OwnerPack value when packName is
// non-empty (source-backed pack path).
func TestUpsertManagedPageLayoutSetsOwnerPackForSourceBacked(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Widget_default",
		ObjectAPIName: "Widget",
		LayoutName:    "Widget Layout",
	}

	// The INSERT must include OwnerPack in the column list and bind a non-NULL value.
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ID).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectQuery(`SELECT`).
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectExec(`INSERT INTO _System_PageLayout`).
		WithArgs(
			layout.ID, layout.ObjectAPIName, layout.LayoutName, layout.Type,
			layout.IsDefault,
			sqlmock.AnyArg(), // configJSON
			"curated",        // source
			"pack-a",         // OwnerPack (non-NULL for source-backed)
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := UpsertManagedPageLayout(context.Background(), db, layout, "curated", "pack-a"); err != nil {
		t.Fatalf("UpsertManagedPageLayout(pack-a) error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestUpsertManagedPageLayoutRejectsEmptyOwnerPack(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Widget_default",
		ObjectAPIName: "Widget",
		LayoutName:    "Widget Layout",
	}

	err = UpsertManagedPageLayout(context.Background(), db, layout, "curated", "")
	if err == nil {
		t.Fatal("UpsertManagedPageLayout() error = nil, want empty owner-pack refusal")
	}
	if !strings.Contains(err.Error(), "requires non-empty owner pack") {
		t.Fatalf("error = %v, want empty owner-pack label", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

// TestInsertWildcardLayoutAssignmentSetsOwnerPackForSourceBacked asserts that
// InsertWildcardLayoutAssignment passes a non-NULL OwnerPack value when packName
// is non-empty (source-backed pack path).
func TestInsertWildcardLayoutAssignmentSetsOwnerPackForSourceBacked(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectExec(`INSERT INTO _System_LayoutAssignment`).
		WithArgs(
			"assign-1", "Widget", "layout-1", "managed_seed",
			"pack-a", // OwnerPack (non-NULL for source-backed)
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := InsertWildcardLayoutAssignment(context.Background(), db, "assign-1", "Widget", "layout-1", "managed_seed", "pack-a"); err != nil {
		t.Fatalf("InsertWildcardLayoutAssignment(pack-a) error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

// TestInsertWildcardLayoutAssignmentSetsNullOwnerPackForAdminPath asserts that
// InsertWildcardLayoutAssignment passes NULL OwnerPack when packName is "" (admin path).
func TestInsertWildcardLayoutAssignmentSetsNullOwnerPackForAdminPath(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectExec(`INSERT INTO _System_LayoutAssignment`).
		WithArgs(
			"assign-1", "Widget", "layout-1", "admin_authored",
			nil, // OwnerPack = NULL for admin path
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := InsertWildcardLayoutAssignment(context.Background(), db, "assign-1", "Widget", "layout-1", "admin_authored", ""); err != nil {
		t.Fatalf("InsertWildcardLayoutAssignment(admin path) error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

// TestNullableOwnerPackReturnsNilForEmpty asserts that nullableOwnerPack("")
// and nullableOwnerPack("  ") return nil (NULL), enabling the admin path to
// match rows with OwnerPack IS NULL via COALESCE(OwnerPack,”)=”.
func TestNullableOwnerPackReturnsNilForEmpty(t *testing.T) {
	for _, input := range []string{"", "   "} {
		got := nullableOwnerPack(input)
		if got != nil {
			t.Errorf("nullableOwnerPack(%q) = %v, want nil", input, got)
		}
	}
}

// TestNullableOwnerPackReturnsStringForNonEmpty asserts that nullableOwnerPack
// with a non-empty pack name returns the pack name (not nil).
func TestNullableOwnerPackReturnsStringForNonEmpty(t *testing.T) {
	got := nullableOwnerPack("pack-a")
	s, ok := got.(string)
	if !ok || s != "pack-a" {
		t.Errorf("nullableOwnerPack(%q) = %v, want string %q", "pack-a", got, "pack-a")
	}
}

func TestLoadPageLayoutRowByBusinessKeyRejectsDuplicateLiveRows(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT`).
		WithArgs("Quote", "Renewal Layout").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("layout-1", `{"layout":"one"}`, "curated", false, false, "pack-a").
			AddRow("layout-2", `{"layout":"two"}`, "curated", false, false, "pack-a"))

	_, err = LoadPageLayoutRowByBusinessKey(context.Background(), db, "Quote", "Renewal Layout")
	if err == nil {
		t.Fatal("LoadPageLayoutRowByBusinessKey() error = nil, want duplicate business-key refusal")
	}
	if !strings.Contains(err.Error(), "has 2 live rows") {
		t.Fatalf("error = %v, want duplicate business-key label", err)
	}
}

func TestLoadPageLayoutRowByBusinessKeyRejectsAdminOwnedSameNameRow(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT`).
		WithArgs("Quote", "Renewal Layout").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("layout-admin", `{"layout":"admin"}`, "admin_created", false, false, ""))

	_, err = LoadPageLayoutRowByBusinessKey(context.Background(), db, "Quote", "Renewal Layout")
	if err == nil {
		t.Fatal("LoadPageLayoutRowByBusinessKey() error = nil, want admin-owned same-name refusal")
	}
	if !strings.Contains(err.Error(), "admin-owned same-name row") {
		t.Fatalf("error = %v, want admin-owned same-name label", err)
	}
}

func TestLoadPageLayoutRowByBusinessKeyRejectsMalformedManagedSameNameRow(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT`).
		WithArgs("Quote", "Renewal Layout").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("layout-managed-malformed", `{"layout":"managed"}`, "curated", false, false, ""))

	_, err = LoadPageLayoutRowByBusinessKey(context.Background(), db, "Quote", "Renewal Layout")
	if err == nil {
		t.Fatal("LoadPageLayoutRowByBusinessKey() error = nil, want malformed managed same-name refusal")
	}
	if !strings.Contains(err.Error(), "malformed managed row") {
		t.Fatalf("error = %v, want malformed managed-row label", err)
	}
}

func TestLoadPageLayoutRowByBusinessKeyRejectsLaterConflictingRowPastFirstTwo(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery(`SELECT`).
		WithArgs("Quote", "Renewal Layout").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("layout-managed", `{"layout":"managed"}`, "curated", false, false, "pack-a").
			AddRow("layout-managed-older", `{"layout":"managed-older"}`, "curated", false, false, "pack-a").
			AddRow("layout-admin", `{"layout":"admin"}`, "admin_created", false, false, ""))

	_, err = LoadPageLayoutRowByBusinessKey(context.Background(), db, "Quote", "Renewal Layout")
	if err == nil {
		t.Fatal("LoadPageLayoutRowByBusinessKey() error = nil, want later conflicting row refusal")
	}
	if !strings.Contains(err.Error(), "admin-owned same-name row") && !strings.Contains(err.Error(), "has 2 live rows") {
		t.Fatalf("error = %v, want same-name conflict label", err)
	}
}

// TestAdoptWildcardLayoutRouteAtomically_AdminAuthoredRouteClearsOwnerPack verifies
// that AdoptWildcardLayoutRouteAtomically writes OwnerPack=NULL on the assignment
// when RouteSource=admin_authored, even when a non-empty packName is supplied.
// This is the regression guard for round-2 finding 2 (curated_page_layout_reconcile.go:186-188).
func TestAdoptWildcardLayoutRouteAtomically_AdminAuthoredRouteClearsOwnerPack(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectBegin()
	// clearOtherPageLayoutDefaultFlagsTx
	mock.ExpectExec("UPDATE _System_PageLayout").WillReturnResult(sqlmock.NewResult(0, 0))
	// setPageLayoutDefaultFlagByIDTx
	mock.ExpectExec("UPDATE _System_PageLayout").WillReturnResult(sqlmock.NewResult(1, 1))
	// DELETE wildcard assignments
	mock.ExpectExec("DELETE FROM _System_LayoutAssignment").WillReturnResult(sqlmock.NewResult(0, 1))
	// INSERT assignment: OwnerPack must be nil (NULL) for admin_authored
	mock.ExpectExec("INSERT INTO _System_LayoutAssignment").
		WithArgs("assign-1", "Widget", "layout-1", models.LayoutAssignmentRouteSourceAdminAuthored, nil).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	if err := AdoptWildcardLayoutRouteAtomically(
		context.Background(), db,
		"assign-1", "Widget", "layout-1",
		models.LayoutAssignmentRouteSourceAdminAuthored, "my-pack",
	); err != nil {
		t.Fatalf("AdoptWildcardLayoutRouteAtomically error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

// TestEnsureManagedPageLayoutAndAdoptWildcardRoute_AdminAuthoredAssignmentClearsOwnerPack
// verifies that EnsureManagedPageLayoutAndAdoptWildcardRouteAtomically writes the
// layout row with OwnerPack=packName but the assignment row with OwnerPack=NULL when
// RouteSource=admin_authored.
// This is the regression guard for round-2 finding 1 (curated_page_layout_reconcile.go:149-152).
func TestEnsureManagedPageLayoutAndAdoptWildcardRoute_AdminAuthoredAssignmentClearsOwnerPack(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Widget_default",
		ObjectAPIName: "Widget",
		LayoutName:    "Widget Layout",
	}

	mock.ExpectBegin()
	// upsertManagedPageLayoutTx — layout row uses packName (not nil)
	mock.ExpectQuery("SELECT `Id`, COALESCE").
		WithArgs(layout.ID).
		WillReturnError(sql.ErrNoRows)
	mock.ExpectQuery("SELECT `Id`, COALESCE").
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}))
	mock.ExpectExec("INSERT INTO _System_PageLayout").WillReturnResult(sqlmock.NewResult(1, 1))
	// clearOtherPageLayoutDefaultFlagsTx
	mock.ExpectExec("UPDATE _System_PageLayout").WillReturnResult(sqlmock.NewResult(0, 0))
	// setPageLayoutDefaultFlagByIDTx
	mock.ExpectExec("UPDATE _System_PageLayout").WillReturnResult(sqlmock.NewResult(1, 1))
	// DELETE wildcard assignments
	mock.ExpectExec("DELETE FROM _System_LayoutAssignment").WillReturnResult(sqlmock.NewResult(0, 1))
	// INSERT assignment: OwnerPack must be nil (NULL) for admin_authored route
	mock.ExpectExec("INSERT INTO _System_LayoutAssignment").
		WithArgs("assign-1", "Widget", "admin-owned-layout", models.LayoutAssignmentRouteSourceAdminAuthored, nil).
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	if err := EnsureManagedPageLayoutAndAdoptWildcardRouteAtomically(
		context.Background(), db,
		layout, "curated",
		"assign-1", "Widget", "admin-owned-layout",
		models.LayoutAssignmentRouteSourceAdminAuthored, "my-pack",
	); err != nil {
		t.Fatalf("EnsureManagedPageLayoutAndAdoptWildcardRouteAtomically error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

func TestEnsureManagedPageLayoutAndAdoptWildcardRoute_RejectsLegacySamePackRowID(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	layout := models.PageLayout{
		ID:            "curated_Widget_default",
		ObjectAPIName: "Widget",
		LayoutName:    "Widget Layout",
		IsDefault:     true,
	}

	mock.ExpectBegin()
	mock.ExpectQuery("SELECT `Id`, COALESCE").
		WithArgs(layout.ID).
		WillReturnError(sql.ErrNoRows)
	mock.ExpectQuery("SELECT `Id`, COALESCE").
		WithArgs(layout.ObjectAPIName, layout.LayoutName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack"}).
			AddRow("legacy-widget-layout", `{"objectApiName":"Widget","layoutName":"Widget Layout","isDefault":true}`, "curated", false, true, "my-pack"))
	mock.ExpectRollback()

	if err := EnsureManagedPageLayoutAndAdoptWildcardRouteAtomically(
		context.Background(), db,
		layout, "curated",
		"assign-1", "Widget", layout.ID,
		models.LayoutAssignmentRouteSourceManagedSeed, "my-pack",
	); err == nil {
		t.Fatal("EnsureManagedPageLayoutAndAdoptWildcardRouteAtomically error = nil, want same-pack business-key collision refusal")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

// TestInsertWildcardLayoutAssignment_AdminAuthoredRouteClearsOwnerPack verifies
// that InsertWildcardLayoutAssignment passes OwnerPack=NULL when routeSource=admin_authored,
// even when a non-empty packName is provided.
func TestInsertWildcardLayoutAssignment_AdminAuthoredRouteClearsOwnerPack(t *testing.T) {
	db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherRegexp))
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectExec(`INSERT INTO _System_LayoutAssignment`).
		WithArgs(
			"assign-1", "Widget", "layout-1",
			models.LayoutAssignmentRouteSourceAdminAuthored,
			nil, // OwnerPack = NULL even though packName="my-pack"
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := InsertWildcardLayoutAssignment(
		context.Background(), db,
		"assign-1", "Widget", "layout-1",
		models.LayoutAssignmentRouteSourceAdminAuthored, "my-pack",
	); err != nil {
		t.Fatalf("InsertWildcardLayoutAssignment(admin_authored with packName) error = %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sqlmock expectations unmet: %v", err)
	}
}

// TestLoadPageLayoutRowReadsOwnerPackColumn confirms LoadPageLayoutRow scans
// OwnerPack alongside the other ownership fields.
func TestLoadPageLayoutRowReadsOwnerPackColumn(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock: %v", err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT").
		WithArgs("layout-1").
		WillReturnRows(sqlmock.NewRows([]string{
			"Id", "Config", "Source", "IsCustomized", "IsDefault", "OwnerPack",
		}).AddRow(
			"layout-1", `{"objectApiName":"Widget"}`, "curated", 0, 1, "salesforce-crm-standard",
		))

	row, err := LoadPageLayoutRow(context.Background(), db, "layout-1")
	if err != nil {
		t.Fatalf("LoadPageLayoutRow error = %v", err)
	}
	if row == nil {
		t.Fatal("LoadPageLayoutRow returned nil, want non-nil")
	}
	if row.ID != "layout-1" {
		t.Errorf("row.ID = %q, want layout-1", row.ID)
	}
	if row.OwnerPack != "salesforce-crm-standard" {
		t.Errorf("row.OwnerPack = %q, want salesforce-crm-standard", row.OwnerPack)
	}
}
