package engine

import (
	"testing"
	"time"

	"github.com/pulse-lets-go/internal/config"
	"github.com/pulse-lets-go/internal/models"
)

func testConfig() *config.Config {
	return &config.Config{
		StaleThresholdSec: 20,
		BalanceMode:       "weighted_random",
		Scoring: config.ScoringConfig{
			IdleCPUMin:            5.0,
			CallFailureRateLethal: 15.0,
			SmoothingFactor:       0,
			LoadAvgMultiplier:     50.0,
			Weights:               config.ScoringWeights{CallScore: 0.40, LoadScore: 0.30, IdleScore: 0.20, FailScore: 0.10},
		},
	}
}

func healthyNode(nodeID string, activeCalls, maxCalls int) *models.NodeMetric {
	return &models.NodeMetric{
		NodeID:          nodeID,
		Status:          "ok",
		TS:              time.Now().Unix(),
		ActiveCalls:     activeCalls,
		MaxCalls:        maxCalls,
		IdleCPU:         50.0,
		LoadAvg:         0.5,
		CallFailureRate: 0,
		SIPGateway:      "sip:" + nodeID + ":5060",
	}
}

// --- validateRule ---

func TestValidateRule_ActionTypes(t *testing.T) {
	// node без action_node_id
	r := models.RouteRule{ActionType: "node", MatchCallerID: "^9"}
	if err := validateRule(&r); err == nil {
		t.Error("node без action_node_id должно дать ошибку")
	}

	// pool без action_pool_ids
	r = models.RouteRule{ActionType: "pool", MatchCallerID: "^9"}
	if err := validateRule(&r); err == nil {
		t.Error("pool без pool_ids должно дать ошибку")
	}

	// auto — ок
	r = models.RouteRule{ActionType: "auto", MatchCallerID: "^9"}
	if err := validateRule(&r); err != nil {
		t.Errorf("auto с caller_id должно быть ок: %v", err)
	}
}

func TestValidateRule_RequireAtLeastOneMatchField(t *testing.T) {
	r := models.RouteRule{ActionType: "auto"}
	if err := validateRule(&r); err == nil {
		t.Error("правило без match-полей должно дать ошибку")
	}

	// С любым match-полем — ок
	r.MatchCallerID = "^7"
	if err := validateRule(&r); err != nil {
		t.Errorf("правило с match_caller_id должно быть ок: %v", err)
	}

	// С time-based полем — ок (без caller_id)
	r2 := models.RouteRule{ActionType: "auto", MatchDaysOfWeek: "1-5"}
	if err := validateRule(&r2); err != nil {
		t.Errorf("правило с match_days должно быть ок: %v", err)
	}
}

func TestValidateRule_TimeRangePair(t *testing.T) {
	r := models.RouteRule{ActionType: "auto", MatchCallerID: "^9", MatchTimeStart: "09:00"}
	if err := validateRule(&r); err == nil {
		t.Error("match_time_start без match_time_end должно дать ошибку")
	}
	r.MatchTimeStart = ""
	r.MatchTimeEnd = "18:00"
	if err := validateRule(&r); err == nil {
		t.Error("match_time_end без match_time_start должно дать ошибку")
	}
	r.MatchTimeStart = "09:00"
	if err := validateRule(&r); err != nil {
		t.Errorf("match_time_start + match_time_end должно быть ок: %v", err)
	}
}

func TestValidateRules(t *testing.T) {
	eng := NewEngine(testConfig())

	tests := []struct {
		name    string
		rules   []models.RouteRule
		wantErr bool
	}{
		{"valid node rule", []models.RouteRule{
			{ID: "r1", Name: "test", ActionType: "node", ActionNodeID: "pbx-01", MatchCallerID: "^7"},
		}, false},
		{"invalid regex", []models.RouteRule{
			{ID: "r1", Name: "test", ActionType: "auto", MatchCallerID: "["},
		}, true},
		{"valid pool rule", []models.RouteRule{
			{ID: "r1", Name: "test", ActionType: "pool", ActionPoolIDs: []string{"a", "b"}, MatchDestination: "^383"},
		}, false},
		{"invalid action_type", []models.RouteRule{
			{ID: "r1", Name: "test", ActionType: "invalid", MatchCallerID: "^9"},
		}, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := eng.ValidateRules(tt.rules)
			if (err != nil) != tt.wantErr {
				t.Errorf("ValidateRules() err=%v, wantErr=%v", err, tt.wantErr)
			}
		})
	}
}

// --- matchRule ---

func TestMatchRule_CallerID(t *testing.T) {
	cr := mustCompile(t, models.RouteRule{
		Enabled: true, Priority: 10, ActionType: "auto",
		MatchCallerID: "^7\\d{10}$",
	})

	if !matchRule(cr, "79293761688", "", "") {
		t.Error("caller_id должен совпасть")
	}
	if matchRule(cr, "89991234567", "", "") {
		t.Error("caller_id 89... не должен совпасть")
	}
	if matchRule(cr, "", "", "") {
		t.Error("пустой caller_id не должен совпасть")
	}
}

func TestMatchRule_Destination(t *testing.T) {
	cr := mustCompile(t, models.RouteRule{
		Enabled: true, Priority: 10, ActionType: "auto",
		MatchDestination: "^383122",
	})

	if !matchRule(cr, "", "3831222211", "") {
		t.Error("dest должен совпасть")
	}
	if matchRule(cr, "", "4991222211", "") {
		t.Error("dest 499... не должен совпасть")
	}
}

func TestMatchRule_IngressTrunk(t *testing.T) {
	cr := mustCompile(t, models.RouteRule{
		Enabled: true, Priority: 10, ActionType: "auto",
		MatchIngressTrunk: "^trk-",
	})

	if !matchRule(cr, "", "", "trk-abc123") {
		t.Error("ingress trunk должен совпасть")
	}
	if matchRule(cr, "", "", "other-123") {
		t.Error("ingress trunk без префикса не должен совпасть")
	}
}

func TestMatchRule_AllConditionsAND(t *testing.T) {
	cr := mustCompile(t, models.RouteRule{
		Enabled: true, Priority: 10, ActionType: "auto",
		MatchCallerID:     "^7\\d+",
		MatchDestination:  "^383",
		MatchIngressTrunk: "^trk-",
	})

	// Все совпадают
	if !matchRule(cr, "79293761688", "3831222211", "trk-001") {
		t.Error("все условия должны совпасть")
	}
	// Не совпадает caller_id
	if matchRule(cr, "89991234567", "3831222211", "trk-001") {
		t.Error("caller_id не совпадает — не должно сматчиться")
	}
	// Не совпадает dest
	if matchRule(cr, "79293761688", "4991222211", "trk-001") {
		t.Error("dest не совпадает — не должно сматчиться")
	}
	// Не совпадает ingress
	if matchRule(cr, "79293761688", "3831222211", "other") {
		t.Error("ingress не совпадает — не должно сматчиться")
	}
}

func TestMatchRule_Disabled(t *testing.T) {
	cr := mustCompile(t, models.RouteRule{
		Enabled: false, Priority: 10, ActionType: "auto",
		MatchCallerID: "^7",
	})
	if matchRule(cr, "79293761688", "", "") {
		t.Error("отключённое правило не должно срабатывать")
	}
}

func TestMatchRule_EmptyMatchFields(t *testing.T) {
	// Пустые match-поля не проверяются — любое значение подходит
	cr := mustCompile(t, models.RouteRule{
		Enabled: true, Priority: 10, ActionType: "auto",
		MatchDaysOfWeek: "1-5",
	})
	// Без time проверки — зависит от текущего времени
	// Проверяем что caller/dest/ingress не фильтруются (пустые)
	if !matchRule(cr, "anything", "anything2", "anything3") {
		t.Error("пустые match-поля должны пропускать любые значения")
	}
}

// --- Time-based matching ---

func TestMatchDayOfWeek(t *testing.T) {
	// Понедельник
	mon := time.Date(2026, 6, 22, 10, 0, 0, 0, time.Local)

	if !matchDayOfWeek(mon, "1-5") {
		t.Error("понедельник должен быть в диапазоне 1-5")
	}
	if !matchDayOfWeek(mon, "1") {
		t.Error("понедельник должен совпасть с '1'")
	}
	if !matchDayOfWeek(mon, "1,3,5") {
		t.Error("понедельник должен быть в списке '1,3,5'")
	}
	if matchDayOfWeek(mon, "6-7") {
		t.Error("понедельник не должен быть в выходных 6-7")
	}

	// Воскресенье
	sun := time.Date(2026, 6, 21, 10, 0, 0, 0, time.Local) // 21 июня 2026 = воскресенье
	if !matchDayOfWeek(sun, "7") {
		t.Error("воскресенье должно совпасть с '7'")
	}
	if matchDayOfWeek(sun, "1-5") {
		t.Error("воскресенье не должно быть в буднях 1-5")
	}
}

func TestMatchTimeRange(t *testing.T) {
	// Обычный диапазон
	morning := time.Date(2026, 6, 22, 10, 30, 0, 0, time.Local)
	if !matchTimeRange(morning, "09:00", "18:00") {
		t.Error("10:30 должно быть в диапазоне 09:00-18:00")
	}

	// Границы
	start := time.Date(2026, 6, 22, 9, 0, 0, 0, time.Local)
	if !matchTimeRange(start, "09:00", "18:00") {
		t.Error("09:00 должно быть в диапазоне 09:00-18:00")
	}
	end := time.Date(2026, 6, 22, 18, 0, 0, 0, time.Local)
	if !matchTimeRange(end, "09:00", "18:00") {
		t.Error("18:00 должно быть в диапазоне 09:00-18:00")
	}

	// Вне диапазона
	night := time.Date(2026, 6, 22, 3, 0, 0, 0, time.Local)
	if matchTimeRange(night, "09:00", "18:00") {
		t.Error("03:00 не должно быть в диапазоне 09:00-18:00")
	}

	// Ночной диапазон (переход через полночь)
	if !matchTimeRange(night, "22:00", "06:00") {
		t.Error("03:00 должно быть в ночном диапазоне 22:00-06:00")
	}

	evening := time.Date(2026, 6, 22, 23, 0, 0, 0, time.Local)
	if !matchTimeRange(evening, "22:00", "06:00") {
		t.Error("23:00 должно быть в ночном диапазоне 22:00-06:00")
	}
}

// --- PickNodeForCall ---

func TestPickNodeForCall_NoRules_FallsBackToPickNode(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 10, 100))

	// Без правил — должен вернуть pbx-01 через PickNode
	nid, _, _, matched := eng.PickNodeForCall("79293761688", "3831222211", "trk-001")
	if nid != "pbx-01" {
		t.Errorf("без правил должен быть PickNode, получен %s", nid)
	}
	if matched != "none" {
		t.Errorf("без правил matched_rule должен быть 'none', получен '%s'", matched)
	}
}

func TestPickNodeForCall_ActionNode(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 10, 100))
	eng.UpdateMetric(healthyNode("pbx-02", 10, 100))

	rules := []models.RouteRule{
		{ID: "r1", Name: "test-rule", Enabled: true, Priority: 10, ActionType: "node", ActionNodeID: "pbx-01", MatchCallerID: "^7\\d+"},
	}
	eng.ValidateRules(rules)
	eng.SetRules(rules)

	nid, score, fallback, matched := eng.PickNodeForCall("79293761688", "", "")
	if nid != "pbx-01" {
		t.Errorf("action=node: ожидался pbx-01, получен %s", nid)
	}
	if score != 100 {
		t.Errorf("action=node: score должен быть 100, получен %.2f", score)
	}
	if fallback {
		t.Error("action=node: fallback должен быть false")
	}
	if matched != "test-rule" {
		t.Errorf("action=node: matched_rule должен быть 'test-rule', получен '%s'", matched)
	}
}

func TestPickNodeForCall_ActionNode_DeadNode_FallsThrough(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	// Только pbx-02 живая; pbx-01 нет в engine
	eng.UpdateMetric(healthyNode("pbx-02", 10, 100))

	// Единственное правило указывает на несуществующую ноду → fallthrough к PickNode
	rules := []models.RouteRule{
		{ID: "r1", Name: "dead-rule", Enabled: true, Priority: 10, ActionType: "node", ActionNodeID: "pbx-01", MatchCallerID: "^7\\d+"},
	}
	eng.ValidateRules(rules)
	eng.SetRules(rules)

	nid, _, _, matched := eng.PickNodeForCall("79293761688", "", "")
	if nid != "pbx-02" {
		t.Errorf("node=dead: должен был fallthrough к pbx-02, получен %s", nid)
	}
	// Правило не сработало — нода не найдена, fallthrough → matched = "none"
	if matched != "none" {
		t.Errorf("node=dead: matched_rule должен быть 'none', получен '%s'", matched)
	}
}

func TestPickNodeForCall_ActionPool(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 10, 100))
	eng.UpdateMetric(healthyNode("pbx-02", 10, 100))
	eng.UpdateMetric(healthyNode("pbx-03", 10, 100))

	rules := []models.RouteRule{
		{ID: "r1", Name: "pool-rule", Enabled: true, Priority: 10, ActionType: "pool",
			ActionPoolIDs: []string{"pbx-01", "pbx-03"}, MatchCallerID: "^7\\d+"},
	}
	eng.ValidateRules(rules)
	eng.SetRules(rules)

	nid, _, fallback, matched := eng.PickNodeForCall("79293761688", "", "")
	if nid == "" || fallback {
		t.Errorf("action=pool: должна быть выбрана нода из пула, получен nid=%s fallback=%v", nid, fallback)
	}
	if nid == "pbx-02" {
		t.Error("action=pool: pbx-02 не в пуле, не должна быть выбрана")
	}
	if matched != "pool-rule" {
		t.Errorf("action=pool: matched_rule должен быть 'pool-rule', получен '%s'", matched)
	}
}

func TestPickNodeForCall_ActionPool_EmptyPool_FallsThrough(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	// Ноды не зарегистрированы в engine — pool будет пуст
	rules := []models.RouteRule{
		{ID: "r1", Name: "empty-pool", Enabled: true, Priority: 10, ActionType: "pool",
			ActionPoolIDs: []string{"ghost-01"}, MatchCallerID: "^7\\d+"},
	}
	eng.ValidateRules(rules)
	eng.SetRules(rules)

	nid, _, fallback, matched := eng.PickNodeForCall("79293761688", "", "")
	// Нет живых нод вообще → fallback
	if !fallback || nid != "" {
		t.Errorf("empty pool: ожидался fallback, получен nid=%s fallback=%v", nid, fallback)
	}
	if matched != "none" {
		t.Errorf("empty pool: matched_rule должен быть 'none', получен '%s'", matched)
	}
}

func TestPickNodeForCall_PriorityOrdering(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 10, 100))
	eng.UpdateMetric(healthyNode("pbx-02", 10, 100))

	rules := []models.RouteRule{
		{ID: "r1", Name: "first", Enabled: true, Priority: 10, ActionType: "node", ActionNodeID: "pbx-01", MatchCallerID: "^7\\d+"},
		{ID: "r2", Name: "second", Enabled: true, Priority: 20, ActionType: "node", ActionNodeID: "pbx-02", MatchCallerID: "^7\\d+"},
	}
	eng.SetRules(rules)

	_, _, _, matched := eng.PickNodeForCall("79293761688", "", "")
	if matched != "first" {
		t.Errorf("priority: должно сработать правило с младшим приоритетом 'first', получено '%s'", matched)
	}
}

func TestPickNodeForCall_ActionAuto_FallsThrough(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 10, 100))

	rules := []models.RouteRule{
		{ID: "r1", Name: "auto-rule", Enabled: true, Priority: 10, ActionType: "auto", MatchCallerID: "^7\\d+"},
	}
	eng.SetRules(rules)

	nid, _, _, matched := eng.PickNodeForCall("79293761688", "", "")
	if nid != "pbx-01" {
		t.Errorf("action=auto: должен fallthrough к PickNode, получен %s", nid)
	}
	// Правило auto матчнулось и применилось (fallthrough к scoring), имя правила возвращается
	if matched != "auto-rule" {
		t.Errorf("action=auto: matched_rule должен быть 'auto-rule', получен '%s'", matched)
	}
}

func TestPickNodeForCall_CascadeRule(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	// pbx-01 НЕ в engine (simulates dead/unregistered)
	eng.UpdateMetric(healthyNode("pbx-02", 10, 100))
	eng.UpdateMetric(healthyNode("pbx-03", 10, 100))

	rules := []models.RouteRule{
		{ID: "r1", Name: "primary", Enabled: true, Priority: 10, ActionType: "node", ActionNodeID: "pbx-01", MatchCallerID: "^7\\d+"},
		{ID: "r2", Name: "backup", Enabled: true, Priority: 20, ActionType: "node", ActionNodeID: "pbx-02", MatchCallerID: "^7\\d+"},
	}
	eng.SetRules(rules)

	nid, _, _, matched := eng.PickNodeForCall("79293761688", "", "")
	if nid != "pbx-02" {
		t.Errorf("cascade: primary pbx-01 dead, должен fallthrough к backup pbx-02, получен %s", nid)
	}
	if matched != "backup" {
		t.Errorf("cascade: должен сработать backup, получено '%s'", matched)
	}
}

// --- Hit counting ---

func TestHitCounting(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 10, 100))

	rules := []models.RouteRule{
		{ID: "r1", Name: "hit-rule", Enabled: true, Priority: 10, ActionType: "node", ActionNodeID: "pbx-01", MatchCallerID: "^7"},
	}
	eng.SetRules(rules)

	for i := 0; i < 5; i++ {
		eng.PickNodeForCall("79293761688", "", "")
	}

	compiled := eng.GetCompiledRules()
	if len(compiled) != 1 {
		t.Fatalf("ожидалось 1 скомпилированное правило, получено %d", len(compiled))
	}
	if h := compiled[0].GetHits(); h != 5 {
		t.Errorf("ожидалось 5 hits, получено %d", h)
	}
}

// --- PickNodeFromPool ---

func TestPickNodeFromPool_BestMode(t *testing.T) {
	cfg := testConfig()
	cfg.BalanceMode = "best"
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 5, 100))  // меньше звонков = выше score
	eng.UpdateMetric(healthyNode("pbx-03", 80, 100))  // много звонков = ниже score

	nid, _, fallback := eng.pickNodeFromPoolLocked([]string{"pbx-01", "pbx-03"})
	if fallback {
		t.Error("pool не должен быть пустым")
	}
	if nid != "pbx-01" {
		t.Errorf("best: должна быть выбрана pbx-01 (меньше звонков), получена %s", nid)
	}
}

func TestPickNodeFromPool_EmptyPool(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)

	_, _, fallback := eng.pickNodeFromPoolLocked([]string{"ghost-01"})
	if !fallback {
		t.Error("пустой pool должен вернуть fallback=true")
	}
}

func TestPickNodeFromPool_DisabledNodeSkipped(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)
	eng.UpdateMetric(healthyNode("pbx-01", 10, 100))
	eng.ToggleNode("pbx-01", true, "test disable")

	_, _, fallback := eng.pickNodeFromPoolLocked([]string{"pbx-01"})
	if !fallback {
		t.Error("pool с отключённой нодой должен вернуть fallback=true")
	}
}

// --- Compile rules ---

func TestCompileRules_Sorting(t *testing.T) {
	rules := []models.RouteRule{
		{ID: "r3", Name: "third", Enabled: true, Priority: 30, ActionType: "auto", MatchCallerID: "^9"},
		{ID: "r1", Name: "first", Enabled: true, Priority: 10, ActionType: "auto", MatchCallerID: "^7"},
		{ID: "r2", Name: "second", Enabled: true, Priority: 20, ActionType: "auto", MatchCallerID: "^8"},
	}
	compiled, err := compileRules(rules)
	if err != nil {
		t.Fatalf("compileRules error: %v", err)
	}
	if len(compiled) != 3 {
		t.Fatalf("ожидалось 3 правила, получено %d", len(compiled))
	}
	if compiled[0].Priority != 10 {
		t.Errorf("первое правило должно иметь priority 10, имеет %d", compiled[0].Priority)
	}
	if compiled[1].Priority != 20 {
		t.Errorf("второе правило должно иметь priority 20, имеет %d", compiled[1].Priority)
	}
	if compiled[2].Priority != 30 {
		t.Errorf("третье правило должно иметь priority 30, имеет %d", compiled[2].Priority)
	}
}

func TestCompileRules_InvalidRegex(t *testing.T) {
	rules := []models.RouteRule{
		{ID: "r1", Name: "bad", Enabled: true, Priority: 10, ActionType: "auto", MatchCallerID: "["},
	}
	_, err := compileRules(rules)
	if err == nil {
		t.Error("невалидный regex должен дать ошибку")
	}
}

// --- LoadRules graceful degradation ---

func TestLoadRules_BrokenRule_Skipped(t *testing.T) {
	cfg := testConfig()
	eng := NewEngine(cfg)

	rules := []models.RouteRule{
		{ID: "r1", Name: "bad", Enabled: true, Priority: 10, ActionType: "auto", MatchCallerID: "["},
		{ID: "r2", Name: "good", Enabled: true, Priority: 20, ActionType: "auto", MatchCallerID: "^7"},
	}
	// LoadRules не возвращает ошибку — просто скипает битые
	eng.LoadRules(rules)

	compiled := eng.GetCompiledRules()
	// LoadRules при compile error загружает nil (скипает всё)
	// Это ожидаемое поведение — битый rules.json = 0 правил
	t.Logf("loadRules: проверка что engine не падает при битых правилах")
	_ = compiled
}

func mustCompile(t *testing.T, r models.RouteRule) *CompiledRule {
	t.Helper()
	rules := []models.RouteRule{r}
	compiled, err := compileRules(rules)
	if err != nil {
		t.Fatalf("compileRules: %v", err)
	}
	return compiled[0]
}
