package rest

import (
	"context"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/nexuscrm/backend/internal/features/audit"
)

func baseFilters(rows int) exportMetaFilters {
	return exportMetaFilters{
		Object:   "Account",
		RecordID: "001xx0000000001",
		Limit:    100,
		Offset:   0,
		Rows:     rows,
	}
}

func TestExportMetaThreshold_DefaultAndOverride(t *testing.T) {
	t.Setenv(envExportMetaThreshold, "")
	if got := exportMetaThreshold(); got != defaultExportThreshold {
		t.Fatalf("default threshold: want %d, got %d", defaultExportThreshold, got)
	}
	t.Setenv(envExportMetaThreshold, "250")
	if got := exportMetaThreshold(); got != 250 {
		t.Fatalf("override threshold: want 250, got %d", got)
	}
	t.Setenv(envExportMetaThreshold, "garbage")
	if got := exportMetaThreshold(); got != defaultExportThreshold {
		t.Fatalf("malformed threshold falls back: want %d, got %d", defaultExportThreshold, got)
	}
	t.Setenv(envExportMetaThreshold, "-5")
	if got := exportMetaThreshold(); got != defaultExportThreshold {
		t.Fatalf("non-positive threshold falls back: want %d, got %d", defaultExportThreshold, got)
	}
}

func TestIsExportMetaEnabled_DefaultsOff(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "")
	if isExportMetaEnabled() {
		t.Fatal("default flag should be off")
	}
	t.Setenv(envExportMetaEnabled, "false")
	if isExportMetaEnabled() {
		t.Fatal("'false' should be off")
	}
	t.Setenv(envExportMetaEnabled, "true")
	if !isExportMetaEnabled() {
		t.Fatal("'true' should be on")
	}
	t.Setenv(envExportMetaEnabled, "1")
	if !isExportMetaEnabled() {
		t.Fatal("'1' should be on")
	}
}

func TestFilterHash_StableAndDistinct(t *testing.T) {
	a := filterHash(baseFilters(100))
	b := filterHash(baseFilters(200)) // Rows not in hash
	if a != b {
		t.Fatalf("row count should not affect hash: %s vs %s", a, b)
	}
	c := baseFilters(100)
	c.Object = "Contact"
	if filterHash(c) == a {
		t.Fatal("different Object must produce different hash")
	}
	if len(a) != 16 {
		t.Fatalf("hash length: want 16 hex, got %d", len(a))
	}
}

// emitExportAudit tests — gate + threshold + emission paths.

func TestEmitExportAudit_FlagOff_NoEmission(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "false")
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	// No mock expectations — any DB call will fail the test.
	emitExportAudit(context.Background(), db, audit.NewDBEmitter(), "005x1", baseFilters(500))
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unexpected DB activity with flag off: %v", err)
	}
}

func TestEmitExportAudit_BelowThreshold_NoEmission(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "true")
	t.Setenv(envExportMetaThreshold, "100")
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	emitExportAudit(context.Background(), db, audit.NewDBEmitter(), "005x1", baseFilters(42))
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unexpected DB activity below threshold: %v", err)
	}
}

func TestEmitExportAudit_AtThreshold_Emits(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "true")
	t.Setenv(envExportMetaThreshold, "100")
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectBegin()
	mock.ExpectExec("INSERT INTO _System_AuditOutbox").
		WillReturnResult(sqlmock.NewResult(7, 1))
	mock.ExpectExec("INSERT INTO _System_AuditLog").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	emitExportAudit(context.Background(), db, audit.NewDBEmitter(), "005x1", baseFilters(100))
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations: %v", err)
	}
}

func TestEmitExportAudit_EmitFailure_Swallowed(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "true")
	t.Setenv(envExportMetaThreshold, "100")
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectBegin()
	mock.ExpectExec("INSERT INTO _System_AuditOutbox").
		WillReturnError(context.DeadlineExceeded)
	mock.ExpectRollback()

	// Must not panic; must not return anything (signature is void). The
	// assertion is purely "does not crash" + "expectations met".
	emitExportAudit(context.Background(), db, audit.NewDBEmitter(), "005x1", baseFilters(500))
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet expectations after emit failure: %v", err)
	}
}

func TestEmitExportAudit_EmptyUserID_SkippedSilently(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "true")
	t.Setenv(envExportMetaThreshold, "100")
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()
	// No mock expectations — empty userID must short-circuit.
	emitExportAudit(context.Background(), db, audit.NewDBEmitter(), "", baseFilters(500))
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unexpected DB activity with empty userID: %v", err)
	}
}

func TestEmitExportAudit_NilDeps_SkippedSilently(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "true")
	t.Setenv(envExportMetaThreshold, "100")
	// Must not panic with nil db / nil emitter.
	emitExportAudit(context.Background(), nil, audit.NewDBEmitter(), "005x1", baseFilters(500))
	emitExportAudit(context.Background(), nil, nil, "005x1", baseFilters(500))
}

// TestDispatchExportAudit_SurvivesHandlerCtxCancel verifies MEDIUM-1:
// dispatchExportAudit detaches from the handler's request context via
// context.WithoutCancel so that a cancellation racing the handler's return
// does not abort the best-effort emit. Cancelling the parent ctx BEFORE
// the goroutine runs must still produce a successful commit on the mock.
func TestDispatchExportAudit_SurvivesHandlerCtxCancel(t *testing.T) {
	t.Setenv(envExportMetaEnabled, "true")
	t.Setenv(envExportMetaThreshold, "100")
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock.New: %v", err)
	}
	defer db.Close()

	mock.ExpectBegin()
	mock.ExpectExec("INSERT INTO _System_AuditOutbox").
		WillReturnResult(sqlmock.NewResult(7, 1))
	mock.ExpectExec("INSERT INTO _System_AuditLog").
		WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()

	parentCtx, cancel := context.WithCancel(context.Background())
	dispatchExportAudit(parentCtx, db, audit.NewDBEmitter(), "005x1", baseFilters(500))
	// Cancel the handler's ctx immediately — the goroutine MUST NOT see
	// the cancel (WithoutCancel is load-bearing).
	cancel()

	// Give the goroutine up to 2s to complete. Poll because we can't
	// race the goroutine startup deterministically.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if err := mock.ExpectationsWereMet(); err == nil {
			return // success
		}
		time.Sleep(10 * time.Millisecond)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("dispatched emit did not complete after ctx cancel: %v", err)
	}
}
