package approvalir

import (
	"strings"
	"testing"
)

func TestCriteriaModeIsClosed(t *testing.T) {
	if !CriteriaModeItems.Valid() {
		t.Fatal("items mode reported invalid")
	}
	for _, mode := range []CriteriaMode{"", "formula", "dialect"} {
		if mode.Valid() {
			t.Fatalf("undefined mode %q reported valid", mode)
		}
	}
}

func TestCriteriaOperatorIsClosed(t *testing.T) {
	operators := []CriteriaOperator{
		CriteriaOperatorGreaterOrEqual,
		CriteriaOperatorLessOrEqual,
		CriteriaOperatorGreaterThan,
		CriteriaOperatorLessThan,
		CriteriaOperatorNotEqual,
		CriteriaOperatorNotContain,
		CriteriaOperatorStartsWith,
		CriteriaOperatorContains,
		CriteriaOperatorExcludes,
		CriteriaOperatorIncludes,
		CriteriaOperatorEquals,
	}
	for _, operator := range operators {
		if !operator.Valid() {
			t.Fatalf("defined operator %q reported invalid", operator)
		}
	}
	for _, operator := range []CriteriaOperator{"", "greaterOrEqual", "dialect_equals"} {
		if operator.Valid() {
			t.Fatalf("undefined operator %q reported valid", operator)
		}
	}
}

func TestCriteriaValidateAcceptsStructuredPredicates(t *testing.T) {
	tests := []struct {
		name     string
		criteria *Criteria
	}{
		{
			name: "implicit conjunction and explicit empty value",
			criteria: &Criteria{Mode: CriteriaModeItems, Items: []CriteriaItem{
				{Field: "closed_at", Operation: CriteriaOperatorEquals, Value: ""},
			}},
		},
		{
			name: "precedence and grouping",
			criteria: &Criteria{
				Mode:          CriteriaModeItems,
				BooleanFilter: "(1 OR 2) AND NOT 3",
				Items: []CriteriaItem{
					{Field: "amount", Operation: CriteriaOperatorGreaterThan, Value: "0"},
					{Field: "priority", Operation: CriteriaOperatorEquals, Value: "high"},
					{Field: "archived", Operation: CriteriaOperatorEquals, Value: "true"},
				},
			},
		},
		{
			name: "case insensitive logic and whitespace",
			criteria: &Criteria{
				Mode:          CriteriaModeItems,
				BooleanFilter: "\n not (1 and 2) \t",
				Items: []CriteriaItem{
					{Field: "amount", Operation: CriteriaOperatorGreaterOrEqual, Value: "0"},
					{Field: "amount", Operation: CriteriaOperatorLessOrEqual, Value: "100"},
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if err := test.criteria.Validate(); err != nil {
				t.Fatalf("Validate() error = %v", err)
			}
		})
	}
}

func TestCriteriaValidateRejectsOpaqueFormulaPredicate(t *testing.T) {
	criteria := &Criteria{Formula: "AND(Amount > 1000, ISPICKVAL(StageName, \"Closed Won\"))"}

	err := criteria.Validate()
	if err == nil || !strings.Contains(err.Error(), "require a formula validator") {
		t.Fatalf("Validate() error = %v, want missing-validator rejection", err)
	}
}

func TestCriteriaValidateRejectsInvalidPredicates(t *testing.T) {
	tests := []struct {
		name     string
		criteria *Criteria
		wantErr  string
	}{
		{name: "nil criteria", wantErr: "criteria is nil"},
		{name: "unsupported formula mode", criteria: &Criteria{Mode: "formula"}, wantErr: "invalid mode"},
		{
			name:     "formula with structured mode",
			criteria: &Criteria{Formula: "Amount > 1000", Mode: CriteriaModeItems},
			wantErr:  "must not set a structured mode",
		},
		{
			name:     "formula with boolean filter",
			criteria: &Criteria{Formula: "Amount > 1000", BooleanFilter: "1"},
			wantErr:  "must not include structured items or a boolean filter",
		},
		{
			name: "formula with structured items",
			criteria: &Criteria{Formula: "Amount > 1000", Items: []CriteriaItem{{
				Field: "amount", Operation: CriteriaOperatorGreaterThan, Value: "1000",
			}}},
			wantErr: "must not include structured items or a boolean filter",
		},
		{name: "missing items", criteria: &Criteria{Mode: CriteriaModeItems}, wantErr: "requires at least one item"},
		{
			name:     "blank field",
			criteria: &Criteria{Mode: CriteriaModeItems, Items: []CriteriaItem{{Field: " \t", Operation: CriteriaOperatorEquals}}},
			wantErr:  "field is required",
		},
		{
			name:     "unknown operation",
			criteria: &Criteria{Mode: CriteriaModeItems, Items: []CriteriaItem{{Field: "amount", Operation: "dialect_equals"}}},
			wantErr:  "invalid operation",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.criteria.Validate()
			if err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Fatalf("Validate() error = %v, want containing %q", err, test.wantErr)
			}
		})
	}
}

func TestCriteriaValidateRejectsMalformedBooleanFilters(t *testing.T) {
	items := []CriteriaItem{
		{Field: "amount", Operation: CriteriaOperatorGreaterThan, Value: "0"},
		{Field: "priority", Operation: CriteriaOperatorEquals, Value: "high"},
	}
	tests := []struct {
		name    string
		filter  string
		wantErr string
	}{
		{name: "zero reference", filter: "0", wantErr: "invalid item reference"},
		{name: "out of range reference", filter: "3", wantErr: "out of range 1..2"},
		{name: "missing right operand", filter: "1 OR", wantErr: "expected item reference"},
		{name: "unknown operator", filter: "1 XOR 2", wantErr: "unexpected token"},
		{name: "missing closing parenthesis", filter: "(1 AND 2", wantErr: "expected ')'"},
		{name: "empty parentheses", filter: "()", wantErr: "expected item reference"},
		{name: "adjacent references", filter: "1 2", wantErr: "unexpected token"},
		{name: "logic word without left boundary", filter: "1AND 2", wantErr: "unexpected token"},
		{name: "word suffix", filter: "1 ANDROID 2", wantErr: "unexpected token"},
		{name: "unused declared item", filter: "1", wantErr: "item reference 2 is unused"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			criteria := &Criteria{Mode: CriteriaModeItems, BooleanFilter: test.filter, Items: items}
			err := criteria.Validate()
			if err == nil || !strings.Contains(err.Error(), test.wantErr) {
				t.Fatalf("Validate() error = %v, want containing %q", err, test.wantErr)
			}
		})
	}
}
