package routing_test

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/veylant/ia-gateway/internal/routing"
)

const tenantA = "tenant-a"
const tenantB = "tenant-b"

// newEngine creates an Engine with a MemStore pre-seeded with the given rules.
func newEngine(rules ...routing.RoutingRule) *routing.Engine {
	store := routing.NewMemStore()
	for _, r := range rules {
		_, _ = store.Create(context.Background(), r)
	}
	return routing.New(store, 30*time.Second, zap.NewNop())
}

func rule(tenantID, name string, priority int, action routing.Action, conds ...routing.Condition) routing.RoutingRule {
	return routing.RoutingRule{
		TenantID:   tenantID,
		Name:       name,
		Priority:   priority,
		IsEnabled:  true,
		Action:     action,
		Conditions: conds,
	}
}

func actionFor(provider string, fallbacks ...string) routing.Action {
	return routing.Action{Provider: provider, FallbackProviders: fallbacks}
}

// ─── Basic match ─────────────────────────────────────────────────────────────

func TestEngine_Evaluate_NoRules_ReturnsFalse(t *testing.T) {
	e := newEngine()
	_, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{TenantID: tenantA})
	require.NoError(t, err)
	assert.False(t, matched)
}

func TestEngine_Evaluate_CatchAll_Matches(t *testing.T) {
	e := newEngine(rule(tenantA, "catch-all", 9999, actionFor("openai")))
	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{TenantID: tenantA})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "openai", action.Provider)
}

func TestEngine_Evaluate_NoMatchForTenant_ReturnsFalse(t *testing.T) {
	e := newEngine(rule(tenantA, "r1", 10, actionFor("openai")))
	_, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{TenantID: tenantB})
	require.NoError(t, err)
	assert.False(t, matched)
}

// ─── Sensitivity routing ──────────────────────────────────────────────────────

func TestEngine_CriticalSensitivity_RoutesToOllama(t *testing.T) {
	r := rule(tenantA, "critical → ollama", 10, actionFor("ollama", "openai"),
		routing.Condition{Field: "request.sensitivity", Operator: "gte", Value: "high"},
	)
	e := newEngine(r)
	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:    tenantA,
		Sensitivity: routing.SensitivityCritical,
	})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "ollama", action.Provider)
	assert.Equal(t, []string{"openai"}, action.FallbackProviders)
}

func TestEngine_LowSensitivity_DoesNotMatchHighRule(t *testing.T) {
	r := rule(tenantA, "high → ollama", 10, actionFor("ollama"),
		routing.Condition{Field: "request.sensitivity", Operator: "gte", Value: "high"},
	)
	e := newEngine(r)
	_, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:    tenantA,
		Sensitivity: routing.SensitivityLow,
	})
	require.NoError(t, err)
	assert.False(t, matched)
}

func TestEngine_MediumSensitivity_MatchesMediumRule(t *testing.T) {
	r := rule(tenantA, "medium+", 10, actionFor("mistral"),
		routing.Condition{Field: "request.sensitivity", Operator: "gte", Value: "medium"},
	)
	e := newEngine(r)
	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:    tenantA,
		Sensitivity: routing.SensitivityMedium,
	})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "mistral", action.Provider)
}

// ─── Department routing ───────────────────────────────────────────────────────

func TestEngine_Department_Finance_RoutesToOllama(t *testing.T) {
	r := rule(tenantA, "finance → ollama", 10, actionFor("ollama"),
		routing.Condition{Field: "user.department", Operator: "eq", Value: "finance"},
	)
	e := newEngine(r)
	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:   tenantA,
		Department: "finance",
	})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "ollama", action.Provider)
}

func TestEngine_Department_Engineering_RoutesToAnthropic(t *testing.T) {
	r := rule(tenantA, "eng → anthropic", 10, actionFor("anthropic"),
		routing.Condition{Field: "user.department", Operator: "eq", Value: "engineering"},
	)
	e := newEngine(r)
	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:   tenantA,
		Department: "engineering",
	})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "anthropic", action.Provider)
}

func TestEngine_Department_RH_OtherDept_DoesNotMatch(t *testing.T) {
	r := rule(tenantA, "rh → ollama", 10, actionFor("ollama"),
		routing.Condition{Field: "user.department", Operator: "eq", Value: "rh"},
	)
	e := newEngine(r)
	_, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:   tenantA,
		Department: "marketing",
	})
	require.NoError(t, err)
	assert.False(t, matched)
}

// ─── Role routing ─────────────────────────────────────────────────────────────

func TestEngine_Role_Admin_MatchesAdminRule(t *testing.T) {
	r := rule(tenantA, "admin → anthropic", 10, actionFor("anthropic"),
		routing.Condition{Field: "user.role", Operator: "in", Value: []interface{}{"admin", "manager"}},
	)
	e := newEngine(r)
	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID: tenantA,
		UserRole: "admin",
	})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "anthropic", action.Provider)
}

// ─── Priority ordering ────────────────────────────────────────────────────────

func TestEngine_Priority_LowestWins(t *testing.T) {
	// Two catch-all rules — priority 10 should win over priority 20
	r1 := rule(tenantA, "catch-all-low", 20, actionFor("openai"))
	r2 := rule(tenantA, "catch-all-high", 10, actionFor("anthropic"))
	e := newEngine(r1, r2)
	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{TenantID: tenantA})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "anthropic", action.Provider, "priority 10 should win over priority 20")
}

func TestEngine_Priority_SpecificBeforeCatchAll(t *testing.T) {
	specific := rule(tenantA, "critical → ollama", 5, actionFor("ollama"),
		routing.Condition{Field: "request.sensitivity", Operator: "gte", Value: "high"},
	)
	catchAll := rule(tenantA, "catch-all", 9999, actionFor("openai"))
	e := newEngine(catchAll, specific) // insert out of order; engine should sort

	action, matched, err := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:    tenantA,
		Sensitivity: routing.SensitivityCritical,
	})
	require.NoError(t, err)
	assert.True(t, matched)
	assert.Equal(t, "ollama", action.Provider, "specific rule should take priority over catch-all")
}

// ─── Multiple conditions (AND logic) ─────────────────────────────────────────

func TestEngine_MultipleConditions_AllMustMatch(t *testing.T) {
	r := rule(tenantA, "finance + high sens", 10, actionFor("ollama"),
		routing.Condition{Field: "user.department", Operator: "eq", Value: "finance"},
		routing.Condition{Field: "request.sensitivity", Operator: "gte", Value: "high"},
	)
	e := newEngine(r)

	// Only department matches, not sensitivity
	_, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:    tenantA,
		Department:  "finance",
		Sensitivity: routing.SensitivityLow,
	})
	assert.False(t, matched, "both conditions must match")

	// Both match
	action, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:    tenantA,
		Department:  "finance",
		Sensitivity: routing.SensitivityHigh,
	})
	assert.True(t, matched)
	assert.Equal(t, "ollama", action.Provider)
}

// ─── Token estimate ───────────────────────────────────────────────────────────

func TestEngine_TokenEstimate_LargePrompt_RoutesToOllama(t *testing.T) {
	r := rule(tenantA, "big prompts → ollama", 10, actionFor("ollama"),
		routing.Condition{Field: "request.token_estimate", Operator: "gte", Value: float64(4000)},
	)
	e := newEngine(r)

	_, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:      tenantA,
		TokenEstimate: 100,
	})
	assert.False(t, matched)

	action, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID:      tenantA,
		TokenEstimate: 5000,
	})
	assert.True(t, matched)
	assert.Equal(t, "ollama", action.Provider)
}

// ─── Model routing ────────────────────────────────────────────────────────────

func TestEngine_Model_MatchesPrefix(t *testing.T) {
	r := rule(tenantA, "gpt-4 → azure", 10, actionFor("azure"),
		routing.Condition{Field: "request.model", Operator: "matches", Value: "gpt-4"},
	)
	e := newEngine(r)
	action, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{
		TenantID: tenantA,
		Model:    "gpt-4o-mini",
	})
	assert.True(t, matched)
	assert.Equal(t, "azure", action.Provider)
}

// ─── Disabled rules ───────────────────────────────────────────────────────────

func TestEngine_DisabledRule_NotEvaluated(t *testing.T) {
	store := routing.NewMemStore()
	r, _ := store.Create(context.Background(), routing.RoutingRule{
		TenantID:   tenantA,
		Name:       "disabled",
		Priority:   10,
		IsEnabled:  false, // disabled
		Conditions: []routing.Condition{},
		Action:     routing.Action{Provider: "anthropic"},
	})
	_ = r

	e := routing.New(store, 30*time.Second, zap.NewNop())
	_, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{TenantID: tenantA})
	assert.False(t, matched, "disabled rule must not be evaluated")
}

// ─── Fallback providers ───────────────────────────────────────────────────────

func TestEngine_Action_FallbackProvidersPreserved(t *testing.T) {
	r := rule(tenantA, "with fallbacks", 10,
		routing.Action{Provider: "anthropic", FallbackProviders: []string{"openai", "mistral"}},
	)
	e := newEngine(r)
	action, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{TenantID: tenantA})
	assert.True(t, matched)
	assert.Equal(t, "anthropic", action.Provider)
	assert.Equal(t, []string{"openai", "mistral"}, action.FallbackProviders)
}

// ─── Model override ───────────────────────────────────────────────────────────

func TestEngine_Action_ModelOverride(t *testing.T) {
	r := rule(tenantA, "model override", 10,
		routing.Action{Provider: "ollama", Model: "llama3"},
	)
	e := newEngine(r)
	action, matched, _ := e.Evaluate(context.Background(), &routing.RoutingContext{TenantID: tenantA})
	assert.True(t, matched)
	assert.Equal(t, "llama3", action.Model)
}

// ─── Benchmark ───────────────────────────────────────────────────────────────

func BenchmarkEngine_Evaluate_10Rules(b *testing.B) {
	rules := make([]routing.RoutingRule, 10)
	for i := range rules {
		rules[i] = routing.RoutingRule{
			TenantID:  tenantA,
			Name:      "rule",
			Priority:  (i + 1) * 10,
			IsEnabled: true,
			Action:    routing.Action{Provider: "openai"},
			Conditions: []routing.Condition{
				{Field: "user.department", Operator: "eq", Value: "nonexistent"},
			},
		}
	}
	// Last rule is catch-all to ensure we always match
	rules[9].Conditions = []routing.Condition{}

	e := newEngine(rules...)
	rctx := &routing.RoutingContext{TenantID: tenantA, Department: "finance"}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _, _ = e.Evaluate(context.Background(), rctx)
	}
}
