package recordmutation

import (
	"context"
	"testing"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

type queryStub struct {
	object  string
	filter  string
	limit   int
	scanAll bool
	rows    []contract.Record
}

func (s *queryStub) Query(_ context.Context, req queryspec.QuerySpec, _ *models.UserSession) (*contract.QueryResult, error) {
	s.object = req.ObjectAPIName
	s.filter = req.Filter.Expression
	s.limit = req.Limit
	s.scanAll = req.ScanAll
	return &contract.QueryResult{Records: append([]contract.Record{}, s.rows...), Done: true}, nil
}

func (*queryStub) SearchWithSpec(context.Context, queryspec.SearchSpec, *models.UserSession) ([]contract.Record, error) {
	return nil, nil
}

func (*queryStub) GlobalSearchWithSpec(context.Context, queryspec.GlobalSearchSpec, *models.UserSession) ([]contract.SearchResult, error) {
	return nil, nil
}

func (*queryStub) QueryHistory(context.Context, contract.HistoryQueryRequest, *models.UserSession) (*contract.HistoryQueryResult, error) {
	return nil, nil
}

func TestDenialFieldPresentEqualsRequiresRequestField(t *testing.T) {
	schema := &models.ObjectMetadata{APIName: "Thing", AccessPolicy: &models.ObjectAccessPolicy{
		RecordMutation: &models.RecordMutationAccessPolicy{Deny: []models.RecordMutationDenial{{
			Surfaces: []string{"singleUpdate"},
			Message:  "closed",
			When: &models.RecordMutationCondition{FieldPresentEquals: &models.RecordMutationFieldEqualsCondition{
				Field:           "State",
				Value:           "Active",
				CaseInsensitive: true,
			}},
		}}},
	}}

	denial, err := Denial(context.Background(), Evaluator{}, schema, "singleUpdate", models.SObject{"Name": "Only rename"}, nil)
	if err != nil {
		t.Fatalf("Denial returned error %v, want nil", err)
	}
	if denial != nil {
		t.Fatalf("Denial returned %+v, want nil when State is absent from request", denial)
	}

	denial, err = Denial(context.Background(), Evaluator{}, schema, "singleUpdate", models.SObject{"State": "Active"}, nil)
	if err != nil {
		t.Fatalf("Denial returned error %v, want nil", err)
	}
	if denial == nil {
		t.Fatal("Denial returned nil, want fieldPresentEquals to match request State=Active")
	}
}

func TestDenialFieldPresentEqualsMatchesBooleanWriteShapes(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Thing",
		Fields:  []models.FieldMetadata{{APIName: "IsActive", Type: constants.FieldTypeBoolean}},
		AccessPolicy: &models.ObjectAccessPolicy{
			RecordMutation: &models.RecordMutationAccessPolicy{Deny: []models.RecordMutationDenial{{
				Surfaces: []string{"singleUpdate"},
				Message:  "closed",
				When: &models.RecordMutationCondition{FieldPresentEquals: &models.RecordMutationFieldEqualsCondition{
					Field:           "IsActive",
					Value:           "true",
					CaseInsensitive: true,
				}},
			}}},
		},
	}

	for _, value := range []interface{}{true, float64(1), "true", "TRUE", "1", "t"} {
		denial, err := Denial(context.Background(), Evaluator{}, schema, "singleUpdate", models.SObject{"IsActive": value}, nil)
		if err != nil {
			t.Fatalf("Denial(%T(%v)) returned error %v, want nil", value, value, err)
		}
		if denial == nil {
			t.Fatalf("Denial(%T(%v)) = nil, want boolean-like true to match", value, value)
		}
	}

	for _, value := range []interface{}{false, float64(0), "false", "0", "f", "not-bool"} {
		denial, err := Denial(context.Background(), Evaluator{}, schema, "singleUpdate", models.SObject{"IsActive": value}, nil)
		if err != nil {
			t.Fatalf("Denial(%T(%v)) returned error %v, want nil", value, value, err)
		}
		if denial != nil {
			t.Fatalf("Denial(%T(%v)) = %+v, want nil", value, value, denial)
		}
	}
}

func TestDenialFieldPresentEqualsDoesNotInferBooleanForTextFields(t *testing.T) {
	schema := &models.ObjectMetadata{
		APIName: "Thing",
		Fields:  []models.FieldMetadata{{APIName: "FlagText", Type: constants.FieldTypeString}},
		AccessPolicy: &models.ObjectAccessPolicy{
			RecordMutation: &models.RecordMutationAccessPolicy{Deny: []models.RecordMutationDenial{{
				Surfaces: []string{"singleUpdate"},
				Message:  "closed",
				When: &models.RecordMutationCondition{FieldPresentEquals: &models.RecordMutationFieldEqualsCondition{
					Field:           "FlagText",
					Value:           "true",
					CaseInsensitive: true,
				}},
			}}},
		},
	}

	denial, err := Denial(context.Background(), Evaluator{}, schema, "singleUpdate", models.SObject{"FlagText": "1"}, nil)
	if err != nil {
		t.Fatalf("Denial returned error %v, want nil", err)
	}
	if denial != nil {
		t.Fatalf("Denial = %+v, want nil because non-boolean fields use string equality", denial)
	}
}

func TestDenialRelatedExistsUsesNeutralQuery(t *testing.T) {
	query := &queryStub{rows: []contract.Record{{
		constants.FieldID:                       "child-1",
		constants.FieldProcessAction_ActionType: constants.ProcessActionTypeAlert,
	}}}
	schema := &models.ObjectMetadata{APIName: "Parent", AccessPolicy: &models.ObjectAccessPolicy{
		RecordMutation: &models.RecordMutationAccessPolicy{Deny: []models.RecordMutationDenial{{
			Surfaces: []string{"singleUpdate"},
			Message:  "closed",
			When: &models.RecordMutationCondition{RelatedExists: &models.RecordMutationRelatedExistsCondition{
				ObjectAPIName: constants.TableProcessAction,
				ParentField:   constants.FieldProcessAction_ProcessDefinitionID,
				Match: []models.RecordMutationFieldEqualsCondition{{
					Field:           constants.FieldProcessAction_ActionType,
					Value:           constants.ProcessActionTypeAlert,
					CaseInsensitive: true,
				}},
			}},
		}}},
	}}

	denial, err := Denial(context.Background(), Evaluator{Query: query}, schema, "singleUpdate", models.SObject{constants.FieldID: "parent-1"}, nil)
	if err != nil {
		t.Fatalf("Denial returned error %v, want nil", err)
	}
	if denial == nil {
		t.Fatal("Denial returned nil, want relatedExists match")
	}
	wantFilter := "ProcessDefinitionId == 'parent-1' and LOWER(ActionType) == 'alert'"
	if query.object != constants.TableProcessAction || query.filter != wantFilter || query.limit != 1 {
		t.Fatalf("query=(%q,%q limit=%d), want ProcessAction by pushed-down predicate %q with limit 1", query.object, query.filter, query.limit, wantFilter)
	}
}

func TestDenialRelatedExistsPushesMatchPredicateAndStopsAtFirstHit(t *testing.T) {
	query := &queryStub{rows: []contract.Record{{constants.FieldID: "child-1001"}}}
	schema := &models.ObjectMetadata{APIName: "Parent", AccessPolicy: &models.ObjectAccessPolicy{
		RecordMutation: &models.RecordMutationAccessPolicy{Deny: []models.RecordMutationDenial{{
			Surfaces: []string{"singleUpdate"},
			Message:  "closed",
			When: &models.RecordMutationCondition{RelatedExists: &models.RecordMutationRelatedExistsCondition{
				ObjectAPIName: constants.TableProcessAction,
				ParentField:   constants.FieldProcessAction_ProcessDefinitionID,
				Match: []models.RecordMutationFieldEqualsCondition{{
					Field:           constants.FieldProcessAction_ActionType,
					Value:           constants.ProcessActionTypeAlert,
					CaseInsensitive: true,
				}},
			}},
		}}},
	}}

	denial, err := Denial(context.Background(), Evaluator{Query: query}, schema, "singleUpdate", models.SObject{constants.FieldID: "pd-many-actions"}, nil)
	if err != nil {
		t.Fatalf("Denial returned error %v, want nil", err)
	}
	if denial == nil {
		t.Fatal("Denial returned nil, want first matching child returned by query to deny")
	}
	wantFilter := "ProcessDefinitionId == 'pd-many-actions' and LOWER(ActionType) == 'alert'"
	if query.filter != wantFilter || query.limit != 1 {
		t.Fatalf("query filter=%q limit=%d, want pushed-down existential filter %q with limit 1", query.filter, query.limit, wantFilter)
	}
}

func TestDenialRelatedExistsIncludeDeletedUsesScanAll(t *testing.T) {
	query := &queryStub{rows: []contract.Record{{constants.FieldID: "deleted-child"}}}
	schema := &models.ObjectMetadata{APIName: "Parent", AccessPolicy: &models.ObjectAccessPolicy{
		RecordMutation: &models.RecordMutationAccessPolicy{Deny: []models.RecordMutationDenial{{
			Surfaces: []string{"singleUpdate"},
			Message:  "closed",
			When: &models.RecordMutationCondition{RelatedExists: &models.RecordMutationRelatedExistsCondition{
				ObjectAPIName:  "Child",
				ParentField:    "ParentId",
				IncludeDeleted: true,
			}},
		}}},
	}}

	denial, err := Denial(context.Background(), Evaluator{Query: query}, schema, "singleUpdate", models.SObject{constants.FieldID: "parent-1"}, nil)
	if err != nil {
		t.Fatalf("Denial returned error %v, want nil", err)
	}
	if denial == nil {
		t.Fatal("Denial returned nil, want related child to deny")
	}
	if !query.scanAll {
		t.Fatal("query ScanAll = false, want true when IncludeDeleted is set")
	}
}

func TestDenialRelatedExistsWithoutQueryFailsClosed(t *testing.T) {
	schema := &models.ObjectMetadata{APIName: "Parent", AccessPolicy: &models.ObjectAccessPolicy{
		RecordMutation: &models.RecordMutationAccessPolicy{Deny: []models.RecordMutationDenial{{
			Surfaces: []string{"singleUpdate"},
			Message:  "closed",
			When: &models.RecordMutationCondition{RelatedExists: &models.RecordMutationRelatedExistsCondition{
				ObjectAPIName: "Child",
				ParentField:   "ParentId",
			}},
		}}},
	}}

	denial, err := Denial(context.Background(), Evaluator{}, schema, "singleUpdate", models.SObject{constants.FieldID: "parent-1"}, nil)
	if err != nil {
		t.Fatalf("Denial returned error %v, want nil", err)
	}
	if denial == nil {
		t.Fatal("Denial returned nil, want fail-closed denial when relatedExists has no query evaluator")
	}
}
