package audit

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

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

func TestIsHistoryObject(t *testing.T) {
	cases := map[string]bool{
		"AccountHistory": true,
		"LeadHistory":    true,
		"History":        false, // suffix == whole name: not a History object
		"Account":        false,
		"":               false,
	}
	for in, want := range cases {
		if got := IsHistoryObject(in); got != want {
			t.Errorf("IsHistoryObject(%q)=%v want %v", in, got, want)
		}
	}
}

func TestParentObjectFromHistoryName(t *testing.T) {
	if got := ParentObjectFromHistoryName("AccountHistory"); got != "Account" {
		t.Errorf("got %q want Account", got)
	}
}

func TestBuildHistoryPlan_Basic(t *testing.T) {
	in := HistorySOQLInput{
		ObjectAPIName: "AccountHistory",
		SelectFields:  []string{"Id", "Field", "OldValue", "NewValue", "CreatedDate", "CreatedById"},
	}
	plan, err := BuildHistoryPlan(in)
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if plan.ObjectApiName != "Account" {
		t.Fatalf("parent: got %q want Account", plan.ObjectApiName)
	}
	if len(plan.Projections) != 6 {
		t.Fatalf("projections: got %d want 6", len(plan.Projections))
	}
}

func TestBuildHistoryPlan_NotHistoryObject(t *testing.T) {
	_, err := BuildHistoryPlan(HistorySOQLInput{ObjectAPIName: "Account"})
	if err == nil {
		t.Fatalf("expected error for non-History object")
	}
}

func TestExecuteHistoryPlan_NilDB(t *testing.T) {
	_, err := ExecuteHistoryPlan(context.Background(), nil, HistoryQueryPlan{ObjectApiName: "Account"}, "a1", 0, 0, true)
	if err == nil {
		t.Fatalf("expected error for nil db")
	}
}

func TestExecuteHistoryPlan_HappyPath(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()

	rows := sqlmock.NewRows([]string{"Id", "RecordId", "FieldName", "OldValue", "NewValue", "ChangedAt", "ChangedById"}).
		AddRow("au1", "acc1", "Description", "old", "new", time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), "usr1")

	mock.ExpectQuery("SELECT Id, RecordId, FieldName").
		WithArgs("Account", "__record__", "acc1").
		WillReturnRows(rows)

	plan := HistoryQueryPlan{ObjectApiName: "Account"}
	got, err := ExecuteHistoryPlan(context.Background(), db, plan, "acc1", 50, 0, true)
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if len(got) != 1 {
		t.Fatalf("rows: got %d want 1", len(got))
	}
	if got[0].Field != "Description" || got[0].ParentID != "acc1" {
		t.Fatalf("unexpected row: %+v", got[0])
	}
	if got[0].OldValue == nil || *got[0].OldValue != "old" {
		t.Fatalf("OldValue: %+v", got[0].OldValue)
	}
	if got[0].DataType != "string" {
		t.Fatalf("DataType stub: got %q", got[0].DataType)
	}
}

func TestExecuteHistoryPlan_FLSSuppression(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()

	mock.ExpectQuery("SELECT Id, RecordId, FieldName").
		WithArgs("Account", "__record__", "acc1", "Salary").
		WillReturnRows(sqlmock.NewRows([]string{"Id", "RecordId", "FieldName", "OldValue", "NewValue", "ChangedAt", "ChangedById"}))

	plan := HistoryQueryPlan{
		ObjectApiName: "Account",
		Predicates: []Predicate{
			{FieldName: "Salary", Column: ColumnFieldName, Op: OpNotEqual, Kind: PredicateSuppressField},
		},
	}
	_, err = ExecuteHistoryPlan(context.Background(), db, plan, "acc1", 50, 0, true)
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("mock expectations: %v", err)
	}
}

// Sanity: error-value from FLS is the typed sentinel we document at §4.1.
func TestFieldDeniedByFLSError_IsSentinel(t *testing.T) {
	var err error = &FieldDeniedByFLSError{FieldApiName: "X"}
	var target *FieldDeniedByFLSError
	if !errors.As(err, &target) {
		t.Fatalf("errors.As failed")
	}
	if !strings.Contains(err.Error(), "X") {
		t.Fatalf("error must contain field name, got %q", err.Error())
	}
}
