package controlplane

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"fmt"
	"regexp"
	"strings"
	"testing"

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

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

	expectSimpleMultiPicklistFastPathEmpty(mock, "Opportunity", "Tags__c")
	mock.ExpectQuery(regexp.QuoteMeta(
		"SELECT `Id`, `Tags__c` FROM `Opportunity` WHERE `Tags__c` IS NOT NULL AND JSON_TYPE(`Tags__c`) = 'STRING' AND `Id` > ? ORDER BY `Id` LIMIT ?",
	)).
		WithArgs("", sqlmock.AnyArg()).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c"}).
			AddRow("006000000000001", `"A;B"`))

	mock.ExpectExec(regexp.QuoteMeta(
		"UPDATE `Opportunity` SET `Tags__c` = CASE `Id` WHEN ? THEN CAST(? AS JSON) ELSE `Tags__c` END WHERE (`Id` = ? AND `Tags__c` = CAST(? AS JSON))",
	)).
		WithArgs("006000000000001", `["A","B"]`, "006000000000001", `"A;B"`).
		WillReturnResult(sqlmock.NewResult(0, 1))

	updated, err := backfillMultiPicklistColumn(context.Background(), db, "Opportunity", "Tags__c")
	if err != nil {
		t.Fatalf("backfillMultiPicklistColumn() error = %v, want nil", err)
	}
	if updated != 1 {
		t.Fatalf("updated = %d, want 1", updated)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations unmet: %v", err)
	}
}

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

	expectSimpleMultiPicklistFastPathPage(mock, "Opportunity", "Tags__c", "", []string{"006000000000001", "006000000000002"}, 2)
	mock.ExpectQuery(regexp.QuoteMeta(
		"SELECT `Id`, `Tags__c` FROM `Opportunity` WHERE `Tags__c` IS NOT NULL AND JSON_TYPE(`Tags__c`) = 'STRING' AND `Id` > ? ORDER BY `Id` LIMIT ?",
	)).
		WithArgs("", 10).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c"}).
			AddRow("006000000000003", `"A;B"`))
	mock.ExpectExec(regexp.QuoteMeta(
		"UPDATE `Opportunity` SET `Tags__c` = CASE `Id` WHEN ? THEN CAST(? AS JSON) ELSE `Tags__c` END WHERE (`Id` = ? AND `Tags__c` = CAST(? AS JSON))",
	)).
		WithArgs("006000000000003", `["A","B"]`, "006000000000003", `"A;B"`).
		WillReturnResult(sqlmock.NewResult(0, 1))

	updated, err := backfillMultiPicklistColumnWithPageSize(context.Background(), db, "Opportunity", "Tags__c", 10)
	if err != nil {
		t.Fatalf("backfillMultiPicklistColumnWithPageSize() error = %v, want nil", err)
	}
	if updated != 3 {
		t.Fatalf("updated = %d, want 3", updated)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations unmet: %v", err)
	}
}

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

	cols := []string{"CloudServices__c", "Existing_Database__c"}
	rows := sqlmock.NewRows([]string{"Id"}).
		AddRow("00Q000000000001").
		AddRow("00Q000000000002")
	mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `Id` FROM `Lead` WHERE (%s) AND `Id` > ? ORDER BY `Id` LIMIT ?",
		simpleMultiPicklistAnyFastPathPredicateForTest(cols),
	))).
		WithArgs("", multiPicklistFastBackfillPageSize).
		WillReturnRows(rows)

	placeholders := "?,?"
	args := []driver.Value{"00Q000000000001", "00Q000000000002"}
	mock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf(
		"UPDATE `Lead` SET `CloudServices__c` = CASE WHEN %s THEN %s ELSE `CloudServices__c` END, "+
			"`Existing_Database__c` = CASE WHEN %s THEN %s ELSE `Existing_Database__c` END "+
			"WHERE `Id` IN (%s) AND (%s)",
		simpleMultiPicklistFastPathPredicateForTest("CloudServices__c"),
		simpleMultiPicklistFastPathValueExpressionForTest("CloudServices__c"),
		simpleMultiPicklistFastPathPredicateForTest("Existing_Database__c"),
		simpleMultiPicklistFastPathValueExpressionForTest("Existing_Database__c"),
		placeholders,
		simpleMultiPicklistAnyFastPathPredicateForTest(cols),
	))).
		WithArgs(args...).
		WillReturnResult(sqlmock.NewResult(0, 2))

	updated, err := fastBackfillSimpleMultiPicklistTable(context.Background(), db, "Lead", cols)
	if err != nil {
		t.Fatalf("fastBackfillSimpleMultiPicklistTable() error = %v, want nil", err)
	}
	if updated != 2 {
		t.Fatalf("updated = %d, want 2", updated)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations unmet: %v", err)
	}
}

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

	cols := []string{"Tags__c", "Regions__c"}
	mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `Id`, CASE WHEN %s THEN CAST(`Tags__c` AS CHAR) ELSE NULL END, "+
			"CASE WHEN %s THEN CAST(`Regions__c` AS CHAR) ELSE NULL END "+
			"FROM `Opportunity` WHERE (%s) AND `Id` > ? ORDER BY `Id` LIMIT ?",
		remainingMultiPicklistStringPredicate("Tags__c"),
		remainingMultiPicklistStringPredicate("Regions__c"),
		remainingMultiPicklistAnyStringPredicate(cols),
	))).
		WithArgs("", 10).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c", "Regions__c"}).
			AddRow("006000000000001", `"A;B"`, nil).
			AddRow("006000000000002", nil, `"East;West"`))

	mock.ExpectExec(regexp.QuoteMeta(
		"UPDATE `Opportunity` SET `Tags__c` = CASE `Id` WHEN ? THEN CAST(? AS JSON) ELSE `Tags__c` END WHERE (`Id` = ? AND `Tags__c` = CAST(? AS JSON))",
	)).
		WithArgs("006000000000001", `["A","B"]`, "006000000000001", `"A;B"`).
		WillReturnResult(sqlmock.NewResult(0, 1))

	mock.ExpectExec(regexp.QuoteMeta(
		"UPDATE `Opportunity` SET `Regions__c` = CASE `Id` WHEN ? THEN CAST(? AS JSON) ELSE `Regions__c` END WHERE (`Id` = ? AND `Regions__c` = CAST(? AS JSON))",
	)).
		WithArgs("006000000000002", `["East","West"]`, "006000000000002", `"East;West"`).
		WillReturnResult(sqlmock.NewResult(0, 1))

	updated, err := backfillMultiPicklistTablePagedFallbackWithPageSize(context.Background(), db, "Opportunity", cols, 10)
	if err != nil {
		t.Fatalf("backfillMultiPicklistTablePagedFallbackWithPageSize() error = %v, want nil", err)
	}
	if updated != 2 {
		t.Fatalf("updated = %d, want 2", updated)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations unmet: %v", err)
	}
}

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

	pagedSelect := regexp.QuoteMeta(
		"SELECT `Id`, `Tags__c` FROM `Opportunity` WHERE `Tags__c` IS NOT NULL AND JSON_TYPE(`Tags__c`) = 'STRING' AND `Id` > ? ORDER BY `Id` LIMIT ?",
	)
	updateCAS := regexp.QuoteMeta(
		"UPDATE `Opportunity` SET `Tags__c` = CASE `Id` WHEN ? THEN CAST(? AS JSON) WHEN ? THEN CAST(? AS JSON) ELSE `Tags__c` END WHERE (`Id` = ? AND `Tags__c` = CAST(? AS JSON)) OR (`Id` = ? AND `Tags__c` = CAST(? AS JSON))",
	)
	updateSingleCAS := regexp.QuoteMeta(
		"UPDATE `Opportunity` SET `Tags__c` = CASE `Id` WHEN ? THEN CAST(? AS JSON) ELSE `Tags__c` END WHERE (`Id` = ? AND `Tags__c` = CAST(? AS JSON))",
	)

	expectSimpleMultiPicklistFastPathEmpty(mock, "Opportunity", "Tags__c")
	mock.ExpectQuery(pagedSelect).
		WithArgs("", 2).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c"}).
			AddRow("006000000000001", `"A;B"`).
			AddRow("006000000000002", `"C;D"`))
	mock.ExpectExec(updateCAS).
		WithArgs(
			"006000000000001", `["A","B"]`,
			"006000000000002", `["C","D"]`,
			"006000000000001", `"A;B"`,
			"006000000000002", `"C;D"`,
		).
		WillReturnResult(sqlmock.NewResult(0, 2))

	mock.ExpectQuery(pagedSelect).
		WithArgs("006000000000002", 2).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c"}).
			AddRow("006000000000003", `"E;F"`))
	mock.ExpectExec(updateSingleCAS).
		WithArgs("006000000000003", `["E","F"]`, "006000000000003", `"E;F"`).
		WillReturnResult(sqlmock.NewResult(0, 1))

	updated, err := backfillMultiPicklistColumnWithPageSize(context.Background(), db, "Opportunity", "Tags__c", 2)
	if err != nil {
		t.Fatalf("backfillMultiPicklistColumnWithPageSize() error = %v, want nil", err)
	}
	if updated != 3 {
		t.Fatalf("updated = %d, want 3", updated)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations unmet: %v", err)
	}
}

func expectSimpleMultiPicklistFastPathEmpty(mock sqlmock.Sqlmock, tableName, columnName string) {
	mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `Id` FROM `%s` WHERE %s AND `Id` > ? ORDER BY `Id` LIMIT ?",
		tableName,
		simpleMultiPicklistFastPathPredicateForTest(columnName),
	))).
		WithArgs("", multiPicklistFastBackfillPageSize).
		WillReturnRows(sqlmock.NewRows([]string{"Id"}))
}

func expectSimpleMultiPicklistFastPathPage(mock sqlmock.Sqlmock, tableName, columnName, afterID string, ids []string, rowsAffected int64) {
	rows := sqlmock.NewRows([]string{"Id"})
	for _, id := range ids {
		rows.AddRow(id)
	}
	mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `Id` FROM `%s` WHERE %s AND `Id` > ? ORDER BY `Id` LIMIT ?",
		tableName,
		simpleMultiPicklistFastPathPredicateForTest(columnName),
	))).
		WithArgs(afterID, multiPicklistFastBackfillPageSize).
		WillReturnRows(rows)

	placeholders := strings.TrimRight(strings.Repeat("?,", len(ids)), ",")
	args := make([]driver.Value, 0, len(ids))
	for _, id := range ids {
		args = append(args, id)
	}
	mock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf(
		"UPDATE `%s` SET `%s` = CASE WHEN TRIM(JSON_UNQUOTE(`%s`)) = '' THEN NULL ELSE JSON_ARRAY(JSON_UNQUOTE(`%s`)) END "+
			"WHERE `Id` IN (%s) AND %s",
		tableName,
		columnName,
		columnName,
		columnName,
		placeholders,
		simpleMultiPicklistFastPathPredicateForTest(columnName),
	))).
		WithArgs(args...).
		WillReturnResult(sqlmock.NewResult(0, rowsAffected))
}

func simpleMultiPicklistFastPathPredicateForTest(columnName string) string {
	return fmt.Sprintf(
		"`%s` IS NOT NULL AND JSON_TYPE(`%s`) = 'STRING' "+
			"AND (TRIM(JSON_UNQUOTE(`%s`)) = '' "+
			"OR (JSON_UNQUOTE(`%s`) = TRIM(JSON_UNQUOTE(`%s`)) AND JSON_UNQUOTE(`%s`) NOT LIKE '%%;%%'))",
		columnName,
		columnName,
		columnName,
		columnName,
		columnName,
		columnName,
	)
}

func simpleMultiPicklistFastPathValueExpressionForTest(columnName string) string {
	return fmt.Sprintf(
		"CASE WHEN TRIM(JSON_UNQUOTE(`%s`)) = '' THEN NULL ELSE JSON_ARRAY(JSON_UNQUOTE(`%s`)) END",
		columnName,
		columnName,
	)
}

func simpleMultiPicklistAnyFastPathPredicateForTest(columnNames []string) string {
	var b strings.Builder
	for i, columnName := range columnNames {
		if i > 0 {
			b.WriteString(" OR ")
		}
		b.WriteString("(")
		b.WriteString(simpleMultiPicklistFastPathPredicateForTest(columnName))
		b.WriteString(")")
	}
	return b.String()
}

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

	blank := `""`
	mock.ExpectExec(regexp.QuoteMeta(
		"UPDATE `Opportunity` SET `Tags__c` = CASE `Id` WHEN ? THEN NULL ELSE `Tags__c` END WHERE (`Id` = ? AND `Tags__c` = CAST(? AS JSON))",
	)).
		WithArgs("006000000000001", "006000000000001", blank).
		WillReturnResult(sqlmock.NewResult(0, 1))

	updated, err := bulkUpdateMultiPicklistBackfillBatch(context.Background(), db, "Opportunity", "Tags__c", []multiPicklistBackfillUpdate{
		{id: "006000000000001", previous: blank},
	})
	if err != nil {
		t.Fatalf("bulkUpdateMultiPicklistBackfillBatch() error = %v, want nil", err)
	}
	if updated != 1 {
		t.Fatalf("updated = %d, want 1", updated)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations unmet: %v", err)
	}
}

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

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	updated, err := backfillMultiPicklistColumnWithPageSize(ctx, db, "Opportunity", "Tags__c", 2)
	if err == nil {
		t.Fatal("backfillMultiPicklistColumnWithPageSize() error = nil, want canceled context error")
	}
	if !strings.Contains(err.Error(), "canceled") {
		t.Fatalf("error = %v, want canceled context", err)
	}
	if updated != 0 {
		t.Fatalf("updated = %d, want 0", updated)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations unmet: %v", err)
	}
}

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

	const (
		tableName  = "Thing__c"
		columnName = "Tags__c"
	)
	field := &models.FieldMetadata{
		APIName: columnName,
		Options: models.PicklistOptionsFromStrings([]string{"A", "B"}),
	}

	expectWrapperCandidateQuery(mock, tableName, columnName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c"}).
			AddRow("repairable", `["[\"A\",\"B\"]"]`).
			AddRow("unresolved", `["[\"X\",\"Y\"]"]`))
	mock.ExpectExec(regexp.QuoteMeta("UPDATE `Thing__c` SET `Tags__c` = CAST(? AS JSON) WHERE `Id` = ? AND `Tags__c` = CAST(? AS JSON)")).
		WithArgs(`["A","B"]`, "repairable", `["[\"A\",\"B\"]"]`).
		WillReturnResult(sqlmock.NewResult(0, 1))

	got, err := repairMultiPicklistWrapperColumn(context.Background(), db, tableName, columnName, field)
	if err != nil {
		t.Fatalf("repairMultiPicklistWrapperColumn: %v", err)
	}
	if got.Updated != 1 {
		t.Fatalf("Updated = %d, want 1", got.Updated)
	}
	if got.Unresolved != 1 {
		t.Fatalf("Unresolved = %d, want 1", got.Unresolved)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	const (
		tableName  = "Thing__c"
		columnName = "Tags__c"
	)
	field := &models.FieldMetadata{
		APIName: columnName,
		Options: models.PicklistOptionsFromStrings([]string{"[foo]", `["A","B"]`}),
	}

	expectWrapperCandidateQuery(mock, tableName, columnName).
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c"}).
			AddRow("declared-bracket-literal", `[ "[foo]" ]`).
			AddRow("declared-json-literal", `[ "[\"A\",\"B\"]" ]`))

	got, err := repairMultiPicklistWrapperColumn(context.Background(), db, tableName, columnName, field)
	if err != nil {
		t.Fatalf("repairMultiPicklistWrapperColumn: %v", err)
	}
	if got.Updated != 0 || got.Unresolved != 0 {
		t.Fatalf("repair result = %+v, want Updated=0 Unresolved=0", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `%s`, `%s`, `%s` FROM `%s` WHERE COALESCE(`%s`,0) = 0",
		constants.ColSysObject_ID,
		constants.ColSysObject_APIName,
		constants.FieldSysObject_ReadSource,
		constants.TableObject,
		constants.FieldSysObject_IsDeleted,
	))).
		WillReturnRows(sqlmock.NewRows([]string{
			constants.ColSysObject_ID,
			constants.ColSysObject_APIName,
			constants.FieldSysObject_ReadSource,
		}).AddRow("obj-1", "Thing__c", nil))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT f.`ObjectId`, f.`ApiName`, f.`PhysicalName`, f.`Options` FROM `_System_Field` f")).
		WillReturnRows(sqlmock.NewRows([]string{"ObjectId", "ApiName", "PhysicalName", "Options"}).
			AddRow("obj-1", "Tags__c", "PhysicalTags__c", `["A","B"]`))
	expectWrapperCandidateQuery(mock, "Thing__c", "PhysicalTags__c").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "PhysicalTags__c"}).
			AddRow("repairable", `["[\"A\",\"B\"]"]`))
	mock.ExpectExec(regexp.QuoteMeta("UPDATE `Thing__c` SET `PhysicalTags__c` = CAST(? AS JSON) WHERE `Id` = ? AND `PhysicalTags__c` = CAST(? AS JSON)")).
		WithArgs(`["A","B"]`, "repairable", `["[\"A\",\"B\"]"]`).
		WillReturnResult(sqlmock.NewResult(0, 1))

	got, err := repairMultiPicklistWrapperStorage(context.Background(), db)
	if err != nil {
		t.Fatalf("repairMultiPicklistWrapperStorage: %v", err)
	}
	if got.Updated != 1 || got.Unresolved != 0 {
		t.Fatalf("repair result = %+v, want Updated=1 Unresolved=0", got)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

func expectWrapperCandidateQuery(mock sqlmock.Sqlmock, tableName, columnName string) *sqlmock.ExpectedQuery {
	return mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `Id`, `%s` FROM `%s` WHERE `%s` IS NOT NULL AND JSON_TYPE(`%s`) = 'ARRAY' AND JSON_SEARCH(`%s`, 'one', '[%%') IS NOT NULL",
		columnName,
		tableName,
		columnName,
		columnName,
		columnName,
	)))
}

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

	mock.ExpectQuery(regexp.QuoteMeta("SELECT 1 FROM _System_Migration WHERE Id = ? LIMIT 1")).
		WithArgs(migrationIdMultiPicklistWrapperRepair).
		WillReturnError(sql.ErrNoRows)
	mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `%s`, `%s`, `%s` FROM `%s` WHERE COALESCE(`%s`,0) = 0",
		constants.ColSysObject_ID,
		constants.ColSysObject_APIName,
		constants.FieldSysObject_ReadSource,
		constants.TableObject,
		constants.FieldSysObject_IsDeleted,
	))).
		WillReturnRows(sqlmock.NewRows([]string{
			constants.ColSysObject_ID,
			constants.ColSysObject_APIName,
			constants.FieldSysObject_ReadSource,
		}))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT f.`ObjectId`, f.`ApiName`, f.`PhysicalName`, f.`Options` FROM `_System_Field` f")).
		WillReturnRows(sqlmock.NewRows([]string{"ObjectId", "ApiName", "PhysicalName", "Options"}))
	mock.ExpectExec(regexp.QuoteMeta("INSERT INTO _System_Migration (Id, Version, Description, AppliedAt, CreatedDate, LastModifiedDate)")).
		WithArgs(
			migrationIdMultiPicklistWrapperRepair,
			1,
			"Repair field-aware multi-select picklist double-wrapper JSON array storage when declared options make it safe.",
		).
		WillReturnResult(sqlmock.NewResult(1, 1))

	if err := MigrateMultiPicklistWrapperRepair(context.Background(), db); err != nil {
		t.Fatalf("MigrateMultiPicklistWrapperRepair: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

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

	mock.ExpectQuery(regexp.QuoteMeta("SELECT 1 FROM _System_Migration WHERE Id = ? LIMIT 1")).
		WithArgs(migrationIdMultiPicklistWrapperRepair).
		WillReturnError(sql.ErrNoRows)
	mock.ExpectQuery(regexp.QuoteMeta(fmt.Sprintf(
		"SELECT `%s`, `%s`, `%s` FROM `%s` WHERE COALESCE(`%s`,0) = 0",
		constants.ColSysObject_ID,
		constants.ColSysObject_APIName,
		constants.FieldSysObject_ReadSource,
		constants.TableObject,
		constants.FieldSysObject_IsDeleted,
	))).
		WillReturnRows(sqlmock.NewRows([]string{
			constants.ColSysObject_ID,
			constants.ColSysObject_APIName,
			constants.FieldSysObject_ReadSource,
		}).AddRow("obj-1", "Thing__c", nil))
	mock.ExpectQuery(regexp.QuoteMeta("SELECT f.`ObjectId`, f.`ApiName`, f.`PhysicalName`, f.`Options` FROM `_System_Field` f")).
		WillReturnRows(sqlmock.NewRows([]string{"ObjectId", "ApiName", "PhysicalName", "Options"}).
			AddRow("obj-1", "Tags__c", "Tags__c", `["A","B"]`))
	expectWrapperCandidateQuery(mock, "Thing__c", "Tags__c").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "Tags__c"}).
			AddRow("unresolved", `["[\"X\",\"Y\"]"]`))

	err = MigrateMultiPicklistWrapperRepair(context.Background(), db)
	if err == nil {
		t.Fatal("MigrateMultiPicklistWrapperRepair err = nil, want unresolved-row error")
	}
	if !strings.Contains(err.Error(), "unresolved wrapper-looking row") {
		t.Fatalf("error = %v, want unresolved wrapper-looking row guidance", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}
