package engine

import (
	"testing"
	"time"

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

func defaultCfg() *config.Config {
	return &config.Config{
		StaleThresholdSec: 20,
		Scoring: config.ScoringConfig{
			IdleCPUMin:           5.0,
			CallFailureRateLethal: 15.0,
			Weights: config.ScoringWeights{
				CallScore: 0.40,
				LoadScore: 0.30,
				IdleScore: 0.20,
				FailScore: 0.10,
			},
		},
	}
}

func freshMetric(nodeID string) *models.NodeMetric {
	return &models.NodeMetric{
		NodeID:          nodeID,
		TS:              time.Now().Unix(),
		Status:          "ok",
		ActiveCalls:     42,
		MaxCalls:        250,
		IdleCPU:         35,
		LoadAvg:         0.85,
		CallFailureRate: 0.5,
	}
}

func TestNewEngine(t *testing.T) {
	eng := NewEngine(defaultCfg())
	if eng == nil {
		t.Fatal("ожидался engine, получен nil")
	}
}

func TestUpdateMetric_CreatesNode(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	ns := eng.UpdateMetric(m)

	if ns.NodeID != "pbx-01" {
		t.Errorf("ожидался node_id=pbx-01, получен %s", ns.NodeID)
	}
	if len(eng.nodes) != 1 {
		t.Errorf("ожидалась 1 нода, получено %d", len(eng.nodes))
	}
}

func TestUpdateMetric_PreservesDisabled(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	eng.UpdateMetric(m)

	// Отключаем ноду вручную
	eng.ToggleNode("pbx-01", true, "test")
	eng.UpdateMetric(m)

	ns, _ := eng.GetNodeInfo("pbx-01")
	if !ns.Disabled {
		t.Error("ожидалось disabled=true")
	}
	if ns.DisabledReason != "test" {
		t.Errorf("ожидалась причина 'test', получена '%s'", ns.DisabledReason)
	}

	// Включаем обратно
	eng.ToggleNode("pbx-01", false, "")
	eng.UpdateMetric(m)

	ns, _ = eng.GetNodeInfo("pbx-01")
	if ns.Disabled {
		t.Error("ожидалось disabled=false")
	}
}

func TestScore_LethalDisabled(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	eng.UpdateMetric(m)
	eng.ToggleNode("pbx-01", true, "maintenance")

	ns, _ := eng.GetNodeInfo("pbx-01")
	if ns.Score != -100 {
		t.Errorf("disabled нода должна иметь score=-100, получен %.0f", ns.Score)
	}
}

func TestScore_LethalStatusNotOk(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	m.Status = "down"
	eng.UpdateMetric(m)

	ns, _ := eng.GetNodeInfo("pbx-01")
	if ns.Score != -100 {
		t.Errorf("status==down нода должна иметь score=-100, получен %.0f", ns.Score)
	}
}

func TestScore_LethalStale(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	// Устанавливаем ts на 30 секунд назад (stale_threshold=20)
	m.TS = time.Now().Unix() - 30
	eng.UpdateMetric(m)

	ns, _ := eng.GetNodeInfo("pbx-01")
	if ns.Score != -100 {
		t.Errorf("stale нода должна иметь score=-100, получен %.0f", ns.Score)
	}
}

func TestScore_LethalMaxCalls(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	m.ActiveCalls = 250
	m.MaxCalls = 250
	eng.UpdateMetric(m)

	ns, _ := eng.GetNodeInfo("pbx-01")
	if ns.Score != -100 {
		t.Errorf("нода с исчерпанной ёмкостью должна иметь score=-100, получен %.0f", ns.Score)
	}
}

func TestScore_LethalIdleCPU(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	m.IdleCPU = 2.0
	eng.UpdateMetric(m)

	ns, _ := eng.GetNodeInfo("pbx-01")
	if ns.Score != -100 {
		t.Errorf("нода с idle_cpu < 5 должна иметь score=-100, получен %.0f", ns.Score)
	}
}

func TestScore_LethalFailureRate(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	m.CallFailureRate = 20.0
	eng.UpdateMetric(m)

	ns, _ := eng.GetNodeInfo("pbx-01")
	if ns.Score != -100 {
		t.Errorf("нода с call_failure_rate > 15 должна иметь score=-100, получен %.0f", ns.Score)
	}
}

func TestScore_Healthy(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	ns := eng.UpdateMetric(m)

	if ns.Score <= 0 {
		t.Errorf("здоровая нода должна иметь положительный score, получен %.0f", ns.Score)
	}
}

func TestScore_HealthyFormula(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	m.ActiveCalls = 0
	m.MaxCalls = 100
	m.IdleCPU = 100
	m.LoadAvg = 0
	m.CallFailureRate = 0

	ns := eng.UpdateMetric(m)

	// Все компоненты = 100
	// score = 100*0.4 + 100*0.3 + 100*0.2 + 100*0.1 = 100
	if ns.Score != 100.0 {
		t.Errorf("идеальная нода должна иметь score=100, получен %.1f", ns.Score)
	}
}

func TestScore_WeightedFormula(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	m.ActiveCalls = 50
	m.MaxCalls = 100
	m.IdleCPU = 50
	m.LoadAvg = 1.0
	m.CallFailureRate = 5.0

	ns := eng.UpdateMetric(m)

	// call_score  = 100 - (50/100 * 100) = 50
	// load_score  = 100 - (1.0 * 50) = 50
	// idle_score  = 50
	// fail_score  = 100 - 5 = 95
	// score = 50*0.4 + 50*0.3 + 50*0.2 + 95*0.1 = 20+15+10+9.5 = 54.5
	expected := 54.5

	if ns.Score < expected-0.1 || ns.Score > expected+0.1 {
		t.Errorf("ожидался score=%.1f, получен %.1f", expected, ns.Score)
	}
}

func TestGetBestNode_NoNodes(t *testing.T) {
	eng := NewEngine(defaultCfg())
	_, _, fallback := eng.GetBestNode()
	if !fallback {
		t.Error("без нод должен быть fallback")
	}
}

func TestGetBestNode_OneNode(t *testing.T) {
	eng := NewEngine(defaultCfg())
	eng.UpdateMetric(freshMetric("pbx-01"))

	id, score, fallback := eng.GetBestNode()
	if fallback {
		t.Error("не должно быть fallback с одной здоровой нодой")
	}
	if id != "pbx-01" {
		t.Errorf("ожидалась pbx-01, получена %s", id)
	}
	if score <= 0 {
		t.Errorf("ожидался положительный score, получен %.0f", score)
	}
}

func TestGetBestNode_AllUnhealthy(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m1 := freshMetric("pbx-01")
	m1.Status = "down"
	eng.UpdateMetric(m1)

	m2 := freshMetric("pbx-02")
	m2.ActiveCalls = 500
	m2.MaxCalls = 500
	eng.UpdateMetric(m2)

	_, _, fallback := eng.GetBestNode()
	if !fallback {
		t.Error("все ноды unhealthy — должен быть fallback")
	}
}

func TestGetBestNode_MultipleNodes(t *testing.T) {
	eng := NewEngine(defaultCfg())

	// pbx-01: 80% загружена (меньше score)
	m1 := freshMetric("pbx-01")
	m1.ActiveCalls = 200
	m1.MaxCalls = 250
	eng.UpdateMetric(m1)

	// pbx-02: 20% загружена (выше score)
	m2 := freshMetric("pbx-02")
	m2.ActiveCalls = 20
	m2.MaxCalls = 250
	eng.UpdateMetric(m2)

	id, _, _ := eng.GetBestNode()
	if id != "pbx-02" {
		t.Errorf("ожидалась pbx-02 (меньше загрузки), получена %s", id)
	}
}

func TestGetNodeHistory(t *testing.T) {
	eng := NewEngine(defaultCfg())
	eng.UpdateMetric(freshMetric("pbx-01"))

	// Повторяем обновление для накопления истории
	for i := 0; i < 5; i++ {
		m := freshMetric("pbx-01")
		m.ActiveCalls = i * 10
		eng.UpdateMetric(m)
	}

	history := eng.GetNodeHistory("pbx-01")
	if len(history) < 3 {
		t.Errorf("ожидалось минимум 3 снапшота, получено %d", len(history))
	}
}

func TestGetNodeHistory_NoNode(t *testing.T) {
	eng := NewEngine(defaultCfg())
	history := eng.GetNodeHistory("nonexistent")
	if history != nil {
		t.Error("для несуществующей ноды ожидался nil")
	}
}

func TestToggleNode_NotFound(t *testing.T) {
	eng := NewEngine(defaultCfg())
	err := eng.ToggleNode("nonexistent", true, "test")
	if err == nil {
		t.Error("ожидалась ошибка для несуществующей ноды")
	}
}

func TestUpdateConfig(t *testing.T) {
	eng := NewEngine(defaultCfg())
	m := freshMetric("pbx-01")
	eng.UpdateMetric(m)

	// Меняем конфиг на более строгий
	newCfg := defaultCfg()
	newCfg.Scoring.CallFailureRateLethal = 0.1
	newCfg.Scoring.IdleCPUMin = 50
	eng.UpdateConfig(newCfg)

	ns, _ := eng.GetNodeInfo("pbx-01")
	if ns.Score != -100 {
		t.Errorf("после ужесточения конфига нода должна стать unhealthy, получен score=%.0f", ns.Score)
	}
}

func TestClamp(t *testing.T) {
	tests := []struct {
		v, min, max, expected float64
	}{
		{50, 0, 100, 50},
		{-10, 0, 100, 0},
		{150, 0, 100, 100},
		{0, 0, 0, 0},
	}
	for _, tt := range tests {
		got := clamp(tt.v, tt.min, tt.max)
		if got != tt.expected {
			t.Errorf("clamp(%.0f, %.0f, %.0f) = %.0f, ожидалось %.0f",
				tt.v, tt.min, tt.max, got, tt.expected)
		}
	}
}
