package audit

import (
	"errors"
	"strings"
	"testing"
)

// newPerms constructs a UserPermissions that grants read on the listed
// (object, field) pairs.
func newPerms(readable ...[2]string) UserPermissions {
	m := make(map[string]map[string]bool, len(readable))
	for _, r := range readable {
		obj, f := r[0], r[1]
		if m[obj] == nil {
			m[obj] = make(map[string]bool)
		}
		m[obj][f] = true
	}
	return mapPermissions{reads: m}
}

type mapPermissions struct {
	reads map[string]map[string]bool
}

func (p mapPermissions) HasFieldRead(object, field string) bool {
	if p.reads == nil {
		return false
	}
	fields := p.reads[object]
	if fields == nil {
		return false
	}
	return fields[field]
}

func TestCheckFLSReadAccess(t *testing.T) {
	perms := newPerms([2]string{"Lead", "Email"})
	if !CheckFLSReadAccess(perms, "Lead", "Email") {
		t.Fatalf("expected read on Lead.Email")
	}
	if CheckFLSReadAccess(perms, "Lead", "Salary") {
		t.Fatalf("expected no read on Lead.Salary")
	}
	if CheckFLSReadAccess(nil, "Lead", "Email") {
		t.Fatalf("nil perms must deny")
	}
}

func TestRedactFieldHistoryPlan_AllowedAllFields(t *testing.T) {
	perms := newPerms([2]string{"Lead", "Email"})
	plan := HistoryQueryPlan{
		ObjectApiName: "Lead",
		Projections: []FieldRef{
			{FieldName: "Email", Column: ColumnOldValue},
			{FieldName: "Email", Column: ColumnNewValue},
		},
		Predicates: []Predicate{
			{FieldName: "Email", Column: ColumnOldValue, Op: OpLike, Kind: PredicateValue},
		},
	}
	got, err := RedactFieldHistoryPlan(plan, perms)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(got.Projections) != 2 {
		t.Fatalf("projections: got %d, want 2", len(got.Projections))
	}
	if len(got.Predicates) != 1 {
		t.Fatalf("predicates: got %d, want 1", len(got.Predicates))
	}
	if len(got.DeniedFields) != 0 {
		t.Fatalf("expected no denied fields")
	}
}

func TestRedactFieldHistoryPlan_ProjectionDropped(t *testing.T) {
	perms := newPerms() // reads nothing
	plan := HistoryQueryPlan{
		ObjectApiName: "Lead",
		Projections: []FieldRef{
			{FieldName: "Salary", Column: ColumnOldValue},
			{FieldName: "Salary", Column: ColumnNewValue},
		},
		// no predicates on Salary value — only metadata predicate below
		Predicates: []Predicate{
			{FieldName: "", Column: ColumnChangedAt, Op: OpGreaterThan, Kind: PredicateMetadata},
		},
	}
	got, err := RedactFieldHistoryPlan(plan, perms)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(got.Projections) != 0 {
		t.Fatalf("projections on denied field must be dropped, got %d", len(got.Projections))
	}
	// A FieldName != Salary predicate must have been added so no rows surface.
	hasSuppression := false
	for _, p := range got.Predicates {
		if p.Kind == PredicateSuppressField && p.FieldName == "Salary" {
			hasSuppression = true
		}
	}
	if !hasSuppression {
		t.Fatalf("expected suppression predicate for Salary; got %+v", got.Predicates)
	}
	if len(got.DeniedFields) != 1 || got.DeniedFields[0] != "Salary" {
		t.Fatalf("DeniedFields: got %v, want [Salary]", got.DeniedFields)
	}
}

func TestRedactFieldHistoryPlan_PredicateRejected(t *testing.T) {
	cases := []struct {
		name      string
		predicate Predicate
	}{
		{"LIKE on OldValue", Predicate{FieldName: "Salary", Column: ColumnOldValue, Op: OpLike, Kind: PredicateValue, RawValueRedacted: "UNSAFE_LITERAL"}},
		{"LIKE on NewValue", Predicate{FieldName: "Salary", Column: ColumnNewValue, Op: OpLike, Kind: PredicateValue, RawValueRedacted: "UNSAFE_LITERAL"}},
		{"equality on NewValue", Predicate{FieldName: "Salary", Column: ColumnNewValue, Op: OpEqual, Kind: PredicateValue, RawValueRedacted: "UNSAFE_LITERAL"}},
		{"equality on OldValue", Predicate{FieldName: "Salary", Column: ColumnOldValue, Op: OpEqual, Kind: PredicateValue, RawValueRedacted: "UNSAFE_LITERAL"}},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			perms := newPerms() // lacks Salary read
			plan := HistoryQueryPlan{
				ObjectApiName: "Lead",
				Projections:   []FieldRef{{FieldName: "Salary", Column: ColumnOldValue}},
				Predicates:    []Predicate{tc.predicate},
			}
			_, err := RedactFieldHistoryPlan(plan, perms)
			if err == nil {
				t.Fatalf("expected rejection for %s", tc.name)
			}
			var flsErr *FieldDeniedByFLSError
			if !errors.As(err, &flsErr) {
				t.Fatalf("expected *FieldDeniedByFLSError, got %T: %v", err, err)
			}
			if flsErr.FieldApiName != "Salary" {
				t.Fatalf("FieldApiName: got %q, want Salary", flsErr.FieldApiName)
			}
			if flsErr.HTTPStatus() != 400 {
				t.Fatalf("HTTPStatus: got %d, want 400", flsErr.HTTPStatus())
			}
			if flsErr.Code() != "FIELD_DENIED_BY_FLS" {
				t.Fatalf("Code: got %q, want FIELD_DENIED_BY_FLS", flsErr.Code())
			}
			// Error message must NOT contain the user-supplied value.
			msg := flsErr.Error()
			if strings.Contains(msg, "UNSAFE_LITERAL") {
				t.Fatalf("error must not echo filter value; got %q", msg)
			}
			if !strings.Contains(msg, "Salary") {
				t.Fatalf("error should name field; got %q", msg)
			}
		})
	}
}

func TestRedactFieldHistoryPlan_MetadataPredicateAllowed(t *testing.T) {
	perms := newPerms() // lacks everything
	plan := HistoryQueryPlan{
		ObjectApiName: "Lead",
		Projections:   []FieldRef{},
		Predicates: []Predicate{
			// Predicate on FieldName itself is metadata, always allowed.
			{FieldName: "", Column: ColumnFieldName, Op: OpEqual, Kind: PredicateMetadata},
			{FieldName: "", Column: ColumnChangedAt, Op: OpGreaterThan, Kind: PredicateMetadata},
			{FieldName: "", Column: ColumnChangedById, Op: OpEqual, Kind: PredicateMetadata},
		},
	}
	got, err := RedactFieldHistoryPlan(plan, perms)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(got.Predicates) != 3 {
		t.Fatalf("metadata predicates must pass; got %d", len(got.Predicates))
	}
}

func TestRedactFieldHistoryPlan_MixedAllowedAndDenied(t *testing.T) {
	// User has Email, lacks Salary. Projections on both, predicate on Email only.
	perms := newPerms([2]string{"Lead", "Email"})
	plan := HistoryQueryPlan{
		ObjectApiName: "Lead",
		Projections: []FieldRef{
			{FieldName: "Email", Column: ColumnOldValue},
			{FieldName: "Salary", Column: ColumnOldValue},
		},
		Predicates: []Predicate{
			{FieldName: "Email", Column: ColumnOldValue, Op: OpLike, Kind: PredicateValue},
		},
	}
	got, err := RedactFieldHistoryPlan(plan, perms)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(got.Projections) != 1 || got.Projections[0].FieldName != "Email" {
		t.Fatalf("only Email projection should remain; got %+v", got.Projections)
	}
	hasSuppression := false
	for _, p := range got.Predicates {
		if p.Kind == PredicateSuppressField && p.FieldName == "Salary" {
			hasSuppression = true
		}
	}
	if !hasSuppression {
		t.Fatalf("expected Salary suppression predicate; got %+v", got.Predicates)
	}
}

func TestRedactFieldHistoryPlan_MixedAllowedAndDeniedPredicate(t *testing.T) {
	// Predicate on denied field must reject even when other fields are allowed.
	perms := newPerms([2]string{"Lead", "Email"})
	plan := HistoryQueryPlan{
		ObjectApiName: "Lead",
		Projections:   []FieldRef{{FieldName: "Email", Column: ColumnOldValue}},
		Predicates: []Predicate{
			{FieldName: "Salary", Column: ColumnOldValue, Op: OpLike, Kind: PredicateValue},
		},
	}
	_, err := RedactFieldHistoryPlan(plan, perms)
	if err == nil {
		t.Fatalf("expected rejection; predicate on denied Salary must 400")
	}
	var flsErr *FieldDeniedByFLSError
	if !errors.As(err, &flsErr) {
		t.Fatalf("expected *FieldDeniedByFLSError, got %T", err)
	}
	if flsErr.FieldApiName != "Salary" {
		t.Fatalf("wrong field: %q", flsErr.FieldApiName)
	}
}

func TestRedactFieldHistoryPlan_NilPerms(t *testing.T) {
	plan := HistoryQueryPlan{
		ObjectApiName: "Lead",
		Projections:   []FieldRef{{FieldName: "Email", Column: ColumnOldValue}},
	}
	got, err := RedactFieldHistoryPlan(plan, nil)
	if err != nil {
		t.Fatalf("nil perms should drop projections, not error: %v", err)
	}
	if len(got.Projections) != 0 {
		t.Fatalf("nil perms: projection must be dropped")
	}
}

func TestRedactFieldHistoryPlan_EmptyObject(t *testing.T) {
	_, err := RedactFieldHistoryPlan(HistoryQueryPlan{}, newPerms())
	if err == nil {
		t.Fatalf("empty ObjectApiName must be a planner-stage error")
	}
}
