package audit

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
)

// newTestEmitter returns a DBEmitter with a frozen clock and deterministic id.
func newTestEmitter(fixedID string, fixedTime time.Time) *DBEmitter {
	return &DBEmitter{
		clock: func() time.Time { return fixedTime },
		idGen: func() string { return fixedID },
	}
}

func validEvent() AuditEvent {
	return AuditEvent{
		ObjectApiName: "Account",
		RecordId:      "001xx0000000001",
		Action:        ActionUpdate,
		ChangedById:   "005xx0000000001",
		Entrypoint:    EntrypointRestAPI,
		Outcome:       OutcomeSuccess,
	}
}

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

	fixedTime := time.Date(2026, 4, 24, 12, 0, 0, 0, time.UTC)
	fixedID := "audit-head-uuid"
	e := newTestEmitter(fixedID, fixedTime)

	mock.ExpectBegin()
	mock.ExpectExec("INSERT INTO _System_AuditOutbox").
		WithArgs(sqlmock.AnyArg(), fixedTime).
		WillReturnResult(sqlmock.NewResult(42, 1))
	mock.ExpectExec("INSERT INTO _System_AuditLog").
		WithArgs(
			fixedID, "Account", "001xx0000000001", headFieldNameSentinel,
			"005xx0000000001", fixedTime, fixedTime, fixedTime,
			"UPDATE", nil, "REST_API", nil,
			nil, nil,
			nil, nil,
			nil, nil, nil,
			"SUCCESS", nil, int64(42),
		).
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("Begin: %v", err)
	}
	if err := e.EmitTx(context.Background(), tx, validEvent()); err != nil {
		t.Fatalf("EmitTx: %v", err)
	}
	if err := tx.Commit(); err != nil {
		t.Fatalf("Commit: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}

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

	e := NewDBEmitter()
	tx, _ := db.Begin()
	ev := validEvent()
	ev.Entrypoint = ""
	err = e.EmitTx(context.Background(), tx, ev)
	if err == nil {
		t.Fatal("expected error for zero Entrypoint")
	}
	if !errors.Is(err, ErrMissingRequired) {
		t.Fatalf("got %v, want wraps ErrMissingRequired", err)
	}
}

func TestEmitTx_NilTxRejected(t *testing.T) {
	e := NewDBEmitter()
	if err := e.EmitTx(context.Background(), nil, validEvent()); err == nil {
		t.Fatal("expected error for nil tx")
	}
}

func TestEmitTx_OutboxFailurePropagates(t *testing.T) {
	db, mock, _ := sqlmock.New()
	defer db.Close()

	e := newTestEmitter("x", time.Now())

	mock.ExpectBegin()
	mock.ExpectExec("INSERT INTO _System_AuditOutbox").
		WillReturnError(errors.New("boom"))
	mock.ExpectRollback()

	tx, _ := db.Begin()
	if err := e.EmitTx(context.Background(), tx, validEvent()); err == nil {
		t.Fatal("expected error when outbox insert fails")
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}

func TestEmitTx_HeadFailurePropagates(t *testing.T) {
	db, mock, _ := sqlmock.New()
	defer db.Close()

	e := newTestEmitter("x", time.Now())

	mock.ExpectBegin()
	mock.ExpectExec("INSERT INTO _System_AuditOutbox").
		WillReturnResult(sqlmock.NewResult(7, 1))
	mock.ExpectExec("INSERT INTO _System_AuditLog").
		WillReturnError(errors.New("FK"))
	mock.ExpectRollback()

	tx, _ := db.Begin()
	if err := e.EmitTx(context.Background(), tx, validEvent()); err == nil {
		t.Fatal("expected error when head insert fails")
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}

func TestEmit_WithoutTx_IsRejected(t *testing.T) {
	e := NewDBEmitter()
	if err := e.Emit(context.Background(), validEvent()); err == nil {
		t.Fatal("expected Emit without tx to fail")
	}
	if err := e.EmitBatch(context.Background(), []AuditEvent{validEvent()}); err == nil {
		t.Fatal("expected EmitBatch without tx to fail")
	}
}

func TestEmitBatchTx_AllOrNothing(t *testing.T) {
	db, mock, _ := sqlmock.New()
	defer db.Close()

	e := newTestEmitter("id", time.Now())

	mock.ExpectBegin()
	// With one invalid event in the batch, no INSERTs should fire.
	mock.ExpectRollback()

	tx, _ := db.Begin()
	bad := validEvent()
	bad.ObjectApiName = ""
	err := e.EmitBatchTx(context.Background(), tx, []AuditEvent{validEvent(), bad})
	if err == nil {
		t.Fatal("expected error for invalid batch")
	}
	_ = tx.Rollback()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}

func TestEmitBatchTx_WritesAllValid(t *testing.T) {
	db, mock, _ := sqlmock.New()
	defer db.Close()

	fixedTime := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
	e := newTestEmitter("id", fixedTime)

	mock.ExpectBegin()
	for i := 0; i < 2; i++ {
		mock.ExpectExec("INSERT INTO _System_AuditOutbox").
			WillReturnResult(sqlmock.NewResult(int64(100+i), 1))
		mock.ExpectExec("INSERT INTO _System_AuditLog").
			WillReturnResult(sqlmock.NewResult(0, 1))
	}
	mock.ExpectCommit()

	tx, _ := db.Begin()
	err := e.EmitBatchTx(context.Background(), tx, []AuditEvent{validEvent(), validEvent()})
	if err != nil {
		t.Fatalf("EmitBatchTx: %v", err)
	}
	_ = tx.Commit()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}
