package audit

import (
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"errors"
	"testing"
	"time"
)

func TestStableFieldRowIDUsesUTCSecondSemanticKey(t *testing.T) {
	base := FieldRow{OutboxRowID: 17, FieldName: "Name", ChangedAt: time.Date(2026, 7, 18, 1, 2, 3, 900, time.FixedZone("x", 3600))}
	sameSecond := base
	sameSecond.ChangedAt = base.ChangedAt.Add(500 * time.Millisecond)
	if stableFieldRowID(base) != stableFieldRowID(sameSecond) {
		t.Fatal("stable ID changed within the normalized UTC second")
	}
	different := base
	different.FieldName = "Owner"
	if stableFieldRowID(base) == stableFieldRowID(different) {
		t.Fatal("stable ID ignored FieldName")
	}
	if got := stableFieldRowID(base); len(got) != len("audf_")+64 || got[:5] != "audf_" {
		t.Fatalf("stable ID = %q", got)
	}
}

func TestPrepareFieldRowsRejectsMismatchedAndDuplicateSemanticKeys(t *testing.T) {
	lease := testLease()
	row := testFieldRow(lease)
	row.OutboxRowID++
	if _, err := prepareFieldRows(lease, []FieldRow{row}); !errors.Is(err, ErrOutboxInvalidFieldRows) {
		t.Fatalf("mismatched outbox ID error = %v", err)
	}
	row = testFieldRow(lease)
	duplicate := row
	duplicate.ChangedAt = row.ChangedAt.Add(500 * time.Millisecond)
	if _, err := prepareFieldRows(lease, []FieldRow{row, duplicate}); !errors.Is(err, ErrOutboxInvalidFieldRows) {
		t.Fatalf("duplicate semantic key error = %v", err)
	}
	row.Outcome = ""
	if _, err := prepareFieldRows(lease, []FieldRow{row}); !errors.Is(err, ErrOutboxInvalidFieldRows) {
		t.Fatalf("missing Outcome error = %v", err)
	}
}

const testReadActiveOwnershipSQL = `SELECT DeploymentId, WorkerReleaseDigest, CAST(OwnershipEpoch AS CHAR)
FROM _System_RuntimeWorkOwnership
WHERE WorkFamily = ? AND IsActive = 1`

func TestExactReplayComparesEveryImmutableColumnAndNullState(t *testing.T) {
	lease := testLease()
	prepared, _ := prepareFieldRows(lease, []FieldRow{testFieldRow(lease)})
	expected := prepared[0]
	baseline := exactFieldRow{
		ID: expected.ID, ObjectApiName: expected.ObjectApiName, RecordID: expected.RecordId,
		FieldName: expected.FieldName, OldValue: sql.NullString{String: expected.OldValue, Valid: true},
		NewValue: sql.NullString{String: expected.NewValue, Valid: true}, ChangedByID: expected.ChangedById,
		ChangedAt: expected.ChangedAt, EventAt: sql.NullTime{Time: expected.EventAt, Valid: true},
		HistorySequence:      sql.NullInt64{Int64: *expected.HistorySequence, Valid: true},
		FieldHistoryEligible: sql.NullBool{Bool: *expected.FieldHistoryEligible, Valid: true},
		Entrypoint:           sql.NullString{String: string(expected.Entrypoint), Valid: true},
		Action:               sql.NullString{String: string(expected.Action), Valid: true},
		OutboxRowID:          sql.NullInt64{Int64: expected.OutboxRowID, Valid: true},
		Outcome:              sql.NullString{String: string(expected.Outcome), Valid: true},
	}
	if !exactFieldRowMatches(baseline, expected) {
		t.Fatal("baseline exact row did not match")
	}
	mutations := map[string]func(*exactFieldRow){
		"Id":                   func(g *exactFieldRow) { g.ID += "x" },
		"ObjectApiName":        func(g *exactFieldRow) { g.ObjectApiName += "x" },
		"RecordId":             func(g *exactFieldRow) { g.RecordID += "x" },
		"FieldName":            func(g *exactFieldRow) { g.FieldName += "x" },
		"OldValue":             func(g *exactFieldRow) { g.OldValue.Valid = false },
		"NewValue":             func(g *exactFieldRow) { g.NewValue.String += "x" },
		"ChangedById":          func(g *exactFieldRow) { g.ChangedByID += "x" },
		"ChangedAt":            func(g *exactFieldRow) { g.ChangedAt = g.ChangedAt.Add(time.Second) },
		"EventAt":              func(g *exactFieldRow) { g.EventAt.Valid = false },
		"HistorySequence":      func(g *exactFieldRow) { g.HistorySequence.Int64++ },
		"FieldHistoryEligible": func(g *exactFieldRow) { g.FieldHistoryEligible.Bool = !g.FieldHistoryEligible.Bool },
		"Entrypoint":           func(g *exactFieldRow) { g.Entrypoint.Valid = false },
		"Action":               func(g *exactFieldRow) { g.Action.String += "x" },
		"OutboxRowId":          func(g *exactFieldRow) { g.OutboxRowID.Int64++ },
		"Outcome":              func(g *exactFieldRow) { g.Outcome.String = string(OutcomeFailure) },
	}
	for name, mutate := range mutations {
		t.Run(name, func(t *testing.T) {
			got := baseline
			mutate(&got)
			if exactFieldRowMatches(got, expected) {
				t.Fatalf("immutable %s mismatch accepted", name)
			}
		})
	}
}

func TestExpandFieldRowsCarriesRequiredOutcome(t *testing.T) {
	lease := testLease()
	event := testEvent()
	rows := ExpandFieldRows(event, lease.Row, time.Now)
	if len(rows) != 1 || rows[0].Outcome != OutcomeSuccess {
		t.Fatalf("expanded rows = %#v", rows)
	}
	event.Action = ActionCreate
	rows = ExpandFieldRows(event, lease.Row, time.Now)
	if len(rows) != 1 || rows[0].Outcome != OutcomeSuccess {
		t.Fatalf("sentinel rows = %#v", rows)
	}
}

func testLease() OutboxLease {
	return OutboxLease{
		Row: OutboxRow{
			ID:         17,
			EnqueuedAt: time.Date(2026, 7, 18, 1, 2, 3, 456789000, time.UTC),
			Attempts:   2,
		},
		HolderID:       "host-123-token",
		LeaseExpiresAt: time.Date(2026, 7, 18, 1, 3, 3, 0, time.UTC),
	}
}

func testFieldRow(lease OutboxLease) FieldRow {
	sequence := int64(41)
	eligible := true
	return FieldRow{
		OutboxRowID:          lease.Row.ID,
		ObjectApiName:        "Account",
		RecordId:             "record-1",
		FieldName:            "Name",
		OldValue:             "old",
		NewValue:             "new",
		ChangedById:          "user-1",
		ChangedAt:            lease.Row.EnqueuedAt,
		EventAt:              lease.Row.EnqueuedAt,
		FieldHistoryEligible: &eligible,
		HistorySequence:      &sequence,
		Action:               ActionUpdate,
		Entrypoint:           EntrypointRestAPI,
		Outcome:              OutcomeSuccess,
	}
}

func testEvent() AuditEvent {
	sequence := int64(41)
	eligible := true
	return AuditEvent{
		ObjectApiName: "Account",
		RecordId:      "record-1",
		Action:        ActionUpdate,
		ChangedById:   "user-1",
		Entrypoint:    EntrypointRestAPI,
		Outcome:       OutcomeSuccess,
		FieldDiffs: []FieldDiff{{
			FieldName:            "Name",
			OldValue:             "old",
			NewValue:             "new",
			FieldHistoryEligible: &eligible,
			HistorySequence:      &sequence,
		}},
	}
}

func payloadForTest(t *testing.T) []byte {
	t.Helper()
	payload, err := json.Marshal(testEvent())
	if err != nil {
		t.Fatal(err)
	}
	return payload
}

func fieldRowDriverArgs(row preparedFieldRow) []driver.Value {
	args := fieldRowArgs(row)
	values := make([]driver.Value, len(args))
	for i := range args {
		values[i] = args[i]
	}
	return values
}
