package main

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

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

	"github.com/nexuscrm/backend/internal/features/audit/replay"
)

func TestParseFlags_MinimalDryRun(t *testing.T) {
	cfg, err := parseFlags([]string{"--db-dsn", "user:pw@tcp(h:4000)/db"})
	if err != nil {
		t.Fatalf("parseFlags: %v", err)
	}
	if err := cfg.validate(); err != nil {
		t.Fatalf("validate: %v", err)
	}
	if cfg.limit != 100 || cfg.mode != "tolerant" {
		t.Errorf("defaults wrong: limit=%d mode=%s", cfg.limit, cfg.mode)
	}
}

func TestValidate_TargetDSNWithoutAck(t *testing.T) {
	cfg := &cliConfig{dbDSN: "x", targetDSN: "y", targetURL: "http://z", mode: "tolerant"}
	if err := cfg.validate(); err == nil {
		t.Fatal("expected error when --target-dsn set without --i-understand-this-writes")
	}
}

func TestValidate_TargetDSNWithoutURL(t *testing.T) {
	cfg := &cliConfig{dbDSN: "x", targetDSN: "y", iUnderstandThisWrites: true, mode: "tolerant"}
	if err := cfg.validate(); err == nil {
		t.Fatal("expected error when --target-dsn set without --target-url")
	}
}

func TestValidate_BadMode(t *testing.T) {
	cfg := &cliConfig{dbDSN: "x", mode: "bogus"}
	if err := cfg.validate(); err == nil {
		t.Fatal("expected error on bad mode")
	}
}

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

	rcv := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
	mock.ExpectQuery("FROM _System_RequestCapture").
		WillReturnRows(sqlmock.NewRows([]string{
			"RequestId", "ReceivedAt", "Method", "Path", "Query",
			"Headers", "Body", "ResponseStatus", "UserId", "Entrypoint",
		}).AddRow("req-1", rcv, "POST", "/services/data/v63.0/sobjects/Lead",
			"", "{}", `{"Name":"x"}`, 201, "005", "REST_API"))

	mock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-1").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}).AddRow("Lead", "001", "__record__", "CREATE", "", "", "REST_API"))

	cfg := &cliConfig{dbDSN: "x", mode: "tolerant", limit: 10}
	rep, err := Run(context.Background(), db, nil, cfg, time.Time{}, time.Time{})
	if err != nil {
		t.Fatalf("Run: %v", err)
	}
	if rep.Summary.Captures != 1 {
		t.Fatalf("expected 1 capture, got %+v", rep.Summary)
	}
	if !rep.DryRun {
		t.Errorf("expected DryRun=true")
	}
	if rep.Summary.Matched != 1 {
		t.Errorf("expected Matched=1 in dry-run, got %d", rep.Summary.Matched)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet: %v", err)
	}
}

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

	rcv := time.Now().UTC()
	mock.ExpectQuery("FROM _System_RequestCapture").
		WillReturnRows(sqlmock.NewRows([]string{
			"RequestId", "ReceivedAt", "Method", "Path", "Query",
			"Headers", "Body", "ResponseStatus", "UserId", "Entrypoint",
		}).AddRow("req-orphan", rcv, "GET", "/x", "", "{}", "", 200, "", "REST_API"))

	mock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-orphan").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}))

	cfg := &cliConfig{dbDSN: "x", mode: "tolerant", limit: 10}
	rep, err := Run(context.Background(), db, nil, cfg, time.Time{}, time.Time{})
	if err != nil {
		t.Fatalf("Run: %v", err)
	}
	if rep.Summary.SkippedNoAudit != 1 {
		t.Errorf("expected 1 skip, got %+v", rep.Summary)
	}
}

func TestWriteReport_Stdout(t *testing.T) {
	dir := t.TempDir()
	out := filepath.Join(dir, "report.json")
	r := &Report{GeneratedAt: time.Now()}
	if err := writeReport(r, out); err != nil {
		t.Fatalf("writeReport: %v", err)
	}
	data, err := os.ReadFile(out)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	var back Report
	if err := json.Unmarshal(data, &back); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
}

func TestParseWindow_Defaults(t *testing.T) {
	cfg := &cliConfig{}
	since, until, err := cfg.parseWindow()
	if err != nil {
		t.Fatalf("parseWindow: %v", err)
	}
	if !since.IsZero() {
		t.Errorf("expected zero since")
	}
	if until.IsZero() {
		t.Errorf("expected default until=now")
	}
}

func TestValidate_SingleDB_RequiresSuffix(t *testing.T) {
	// Same DSN for db and target, no suffix → reject.
	cfg := &cliConfig{
		dbDSN:                 "same",
		targetDSN:             "same",
		targetURL:             "http://z",
		iUnderstandThisWrites: true,
		mode:                  "tolerant",
	}
	if err := cfg.validate(); err == nil {
		t.Fatal("expected error when --target-dsn equals --db-dsn without --replay-rid-suffix")
	}
}

func TestValidate_SingleDB_WithSuffixOK(t *testing.T) {
	cfg := &cliConfig{
		dbDSN:                 "same",
		targetDSN:             "same",
		targetURL:             "http://z",
		iUnderstandThisWrites: true,
		mode:                  "tolerant",
		replayRIDSuffix:       "-replay",
	}
	if err := cfg.validate(); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestValidate_DistinctDSNEmptySuffixOK(t *testing.T) {
	cfg := &cliConfig{
		dbDSN:                 "a",
		targetDSN:             "b",
		targetURL:             "http://z",
		iUnderstandThisWrites: true,
		mode:                  "tolerant",
	}
	if err := cfg.validate(); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestValidate_SuffixWithoutTargetDSN(t *testing.T) {
	cfg := &cliConfig{
		dbDSN:           "x",
		mode:            "tolerant",
		replayRIDSuffix: "-replay",
	}
	if err := cfg.validate(); err == nil {
		t.Fatal("expected error when --replay-rid-suffix set without --target-dsn")
	}
}

func TestRun_ActiveReplayWithSuffix_UsesSuffixedRID(t *testing.T) {
	// Source DB mock: returns one capture + its expected audit row keyed by the
	// original rid ("req-1"). Then the target DB mock returns replay audit rows
	// keyed by the suffixed rid ("req-1-replay"). The test asserts that the
	// tool queries target with the suffixed rid, not the raw rid.

	srcDB, srcMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock src: %v", err)
	}
	defer srcDB.Close()

	tgtDB, tgtMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock tgt: %v", err)
	}
	defer tgtDB.Close()

	rcv := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
	srcMock.ExpectQuery("FROM _System_RequestCapture").
		WillReturnRows(sqlmock.NewRows([]string{
			"RequestId", "ReceivedAt", "Method", "Path", "Query",
			"Headers", "Body", "ResponseStatus", "UserId", "Entrypoint",
		}).AddRow("req-1", rcv, "POST", "/services/data/v63.0/sobjects/Lead",
			"", "{}", `{"Name":"x"}`, 201, "005", "REST_API"))

	// Source audit read keyed on ORIGINAL rid.
	srcMock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-1").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}).AddRow("Lead", "001", "__record__", "CREATE", "", "", "REST_API"))

	// Target audit read MUST be keyed on SUFFIXED rid ("req-1-replay").
	// sqlmock WithArgs("req-1-replay") will fail the test if the tool queries
	// with the raw rid instead.
	tgtMock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-1-replay").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}).AddRow("Lead", "001", "__record__", "CREATE", "", "", "REST_API"))

	// Stand up a tiny HTTP server that records the X-Replay-RequestId header.
	var seenHeader string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seenHeader = r.Header.Get("X-Replay-RequestId")
		w.WriteHeader(204)
	}))
	defer srv.Close()

	cfg := &cliConfig{
		dbDSN:                 "src",
		targetDSN:             "tgt",
		targetURL:             srv.URL,
		iUnderstandThisWrites: true,
		mode:                  "tolerant",
		limit:                 10,
		replayRIDSuffix:       "-replay",
	}

	rep, err := Run(context.Background(), srcDB, tgtDB, cfg, time.Time{}, time.Time{})
	if err != nil {
		t.Fatalf("Run: %v", err)
	}
	if rep.Summary.Captures != 1 {
		t.Fatalf("expected 1 capture, got %+v", rep.Summary)
	}
	if rep.DryRun {
		t.Fatalf("expected DryRun=false in active replay mode")
	}
	if seenHeader != "req-1-replay" {
		t.Errorf("expected X-Replay-RequestId=req-1-replay, got %q", seenHeader)
	}
	if len(rep.Entries) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(rep.Entries))
	}
	if rep.Entries[0].ReplayRequestID != "req-1-replay" {
		t.Errorf("expected ReplayRequestID=req-1-replay, got %q", rep.Entries[0].ReplayRequestID)
	}
	if err := srcMock.ExpectationsWereMet(); err != nil {
		t.Errorf("source unmet: %v", err)
	}
	if err := tgtMock.ExpectationsWereMet(); err != nil {
		t.Errorf("target unmet: %v", err)
	}
}

func TestValidate_AuthBearerWithoutTargetDSN(t *testing.T) {
	cfg := &cliConfig{
		dbDSN:            "x",
		mode:             "tolerant",
		replayAuthBearer: "fake-token",
	}
	if err := cfg.validate(); err == nil {
		t.Fatal("expected error when --replay-auth-bearer set without --target-dsn")
	}
}

func TestPostCapture_SendsAuthorizationHeader(t *testing.T) {
	var seenAuth string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seenAuth = r.Header.Get("Authorization")
		w.WriteHeader(204)
	}))
	defer srv.Close()
	c := replay.Capture{Method: "POST", Path: "/x", Body: `{"a":1}`}
	if _, err := postCapture(context.Background(), srv.URL, c, "rid", "fake-token"); err != nil {
		t.Fatalf("postCapture: %v", err)
	}
	if seenAuth != "Bearer fake-token" {
		t.Errorf("expected Authorization=Bearer fake-token, got %q", seenAuth)
	}
}

func TestPostCapture_EmptyBearerOmitsHeader(t *testing.T) {
	var seenAuth string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		seenAuth = r.Header.Get("Authorization")
		w.WriteHeader(204)
	}))
	defer srv.Close()
	c := replay.Capture{Method: "POST", Path: "/x", Body: `{"a":1}`}
	if _, err := postCapture(context.Background(), srv.URL, c, "rid", ""); err != nil {
		t.Fatalf("postCapture: %v", err)
	}
	if seenAuth != "" {
		t.Errorf("expected no Authorization header, got %q", seenAuth)
	}
}

func TestRun_DiffIgnoreRecordID_MatchesDespiteDifferentRIDs(t *testing.T) {
	// Expected has RecordId "001-A"; replay has "001-B"; everything else identical.
	// Without the flag → Match=false. With the flag → Match=true, and the
	// JSON report's Expected / Replay arrays still carry the original RecordIds.

	srcDB, srcMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock src: %v", err)
	}
	defer srcDB.Close()

	tgtDB, tgtMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock tgt: %v", err)
	}
	defer tgtDB.Close()

	rcv := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
	srcMock.ExpectQuery("FROM _System_RequestCapture").
		WillReturnRows(sqlmock.NewRows([]string{
			"RequestId", "ReceivedAt", "Method", "Path", "Query",
			"Headers", "Body", "ResponseStatus", "UserId", "Entrypoint",
		}).AddRow("req-1", rcv, "POST", "/services/data/v63.0/sobjects/Lead",
			"", "{}", `{"Name":"x"}`, 201, "005", "REST_API"))

	srcMock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-1").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}).AddRow("Lead", "001-A", "__record__", "CREATE", "", "", "REST_API"))

	tgtMock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-1-replay").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}).AddRow("Lead", "001-B", "__record__", "CREATE", "", "", "REST_API"))

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(204)
	}))
	defer srv.Close()

	cfg := &cliConfig{
		dbDSN:                 "src",
		targetDSN:             "tgt",
		targetURL:             srv.URL,
		iUnderstandThisWrites: true,
		mode:                  "tolerant",
		limit:                 10,
		replayRIDSuffix:       "-replay",
		diffIgnoreRecordID:    true,
	}

	rep, err := Run(context.Background(), srcDB, tgtDB, cfg, time.Time{}, time.Time{})
	if err != nil {
		t.Fatalf("Run: %v", err)
	}
	if len(rep.Entries) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(rep.Entries))
	}
	entry := rep.Entries[0]
	if !entry.Diff.Match {
		t.Errorf("expected Match=true with diff-ignore-record-id, got Diff=%+v", entry.Diff)
	}
	// Original RecordIds preserved in the report.
	if len(entry.Expected) != 1 || entry.Expected[0].RecordId != "001-A" {
		t.Errorf("expected Expected[0].RecordId=001-A preserved, got %+v", entry.Expected)
	}
	if len(entry.Replay) != 1 || entry.Replay[0].RecordId != "001-B" {
		t.Errorf("expected Replay[0].RecordId=001-B preserved, got %+v", entry.Replay)
	}
	if err := srcMock.ExpectationsWereMet(); err != nil {
		t.Errorf("source unmet: %v", err)
	}
	if err := tgtMock.ExpectationsWereMet(); err != nil {
		t.Errorf("target unmet: %v", err)
	}
}

func TestRun_DiffIgnoreRecordID_OffByDefault(t *testing.T) {
	// Same fixtures as above but flag is false → RecordId difference should
	// cause Match=false.
	srcDB, srcMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock src: %v", err)
	}
	defer srcDB.Close()

	tgtDB, tgtMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("sqlmock tgt: %v", err)
	}
	defer tgtDB.Close()

	rcv := time.Date(2026, 4, 24, 10, 0, 0, 0, time.UTC)
	srcMock.ExpectQuery("FROM _System_RequestCapture").
		WillReturnRows(sqlmock.NewRows([]string{
			"RequestId", "ReceivedAt", "Method", "Path", "Query",
			"Headers", "Body", "ResponseStatus", "UserId", "Entrypoint",
		}).AddRow("req-1", rcv, "POST", "/services/data/v63.0/sobjects/Lead",
			"", "{}", `{"Name":"x"}`, 201, "005", "REST_API"))
	srcMock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-1").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}).AddRow("Lead", "001-A", "__record__", "CREATE", "", "", "REST_API"))
	tgtMock.ExpectQuery("FROM _System_AuditLog").
		WithArgs("req-1-replay").
		WillReturnRows(sqlmock.NewRows([]string{
			"ObjectApiName", "RecordId", "FieldName", "Action",
			"OldValue", "NewValue", "Entrypoint",
		}).AddRow("Lead", "001-B", "__record__", "CREATE", "", "", "REST_API"))

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(204)
	}))
	defer srv.Close()

	cfg := &cliConfig{
		dbDSN:                 "src",
		targetDSN:             "tgt",
		targetURL:             srv.URL,
		iUnderstandThisWrites: true,
		mode:                  "tolerant",
		limit:                 10,
		replayRIDSuffix:       "-replay",
		// diffIgnoreRecordID: false (default)
	}
	rep, err := Run(context.Background(), srcDB, tgtDB, cfg, time.Time{}, time.Time{})
	if err != nil {
		t.Fatalf("Run: %v", err)
	}
	if len(rep.Entries) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(rep.Entries))
	}
	if rep.Entries[0].Diff.Match {
		t.Errorf("expected Match=false without flag (RecordIds differ), got Diff=%+v", rep.Entries[0].Diff)
	}
	if err := srcMock.ExpectationsWereMet(); err != nil {
		t.Errorf("source unmet: %v", err)
	}
	if err := tgtMock.ExpectationsWereMet(); err != nil {
		t.Errorf("target unmet: %v", err)
	}
}

func TestIsLikelyJSON(t *testing.T) {
	if !isLikelyJSON(`{"x":1}`) {
		t.Error("object missed")
	}
	if !isLikelyJSON(`  [1,2,3]`) {
		t.Error("array missed")
	}
	if isLikelyJSON("raw-binary-bytes") {
		t.Error("binary matched as JSON")
	}
}
