package executionpool

import (
	"errors"
	"testing"
	"time"
)

func TestSelectorRejectsUnreachableAndPrefersNetwork(t *testing.T) {
	now := time.Now()
	selector := NewSelector()
	decision, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{
		candidate("slow", RouteHealthy, now, 120*time.Millisecond, 8, 1),
		candidate("fast", RouteHealthy, now, 20*time.Millisecond, 8, 1),
		candidate("down", RouteUnreachable, now, time.Millisecond, 32, 0),
	}})
	if err != nil {
		t.Fatal(err)
	}
	if decision.PoolID != "fast" {
		t.Fatalf("pool=%s, want fast", decision.PoolID)
	}
	if decision.Rejected["down"] != "route_unreachable" {
		t.Fatalf("down rejection=%q", decision.Rejected["down"])
	}
}

func TestSelectorUsesCapacityAsHardGate(t *testing.T) {
	now := time.Now()
	selector := NewSelector()
	decision, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{
		candidate("fast-full", RouteHealthy, now, 10*time.Millisecond, 2, 2),
		candidate("slower-free", RouteHealthy, now, 80*time.Millisecond, 8, 1),
	}})
	if err != nil {
		t.Fatal(err)
	}
	if decision.PoolID != "slower-free" {
		t.Fatalf("pool=%s, want slower-free", decision.PoolID)
	}
}

func TestSelectorRejectsUnknownAndStale(t *testing.T) {
	now := time.Now()
	selector := NewSelector()
	_, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{
		candidate("unknown", RouteUnknown, now, time.Millisecond, 8, 0),
		candidate("stale", RouteHealthy, now.Add(-time.Minute), time.Millisecond, 8, 0),
	}})
	if !errors.Is(err, ErrNoEligiblePool) {
		t.Fatalf("err=%v, want ErrNoEligiblePool", err)
	}
}

func TestSelectorHonorsHysteresis(t *testing.T) {
	now := time.Now()
	selector := NewSelector()
	fast := candidate("fast", RouteHealthy, now, 20*time.Millisecond, 8, 1)
	fast.BetterWindows = 3
	current := candidate("current", RouteHealthy, now, 80*time.Millisecond, 8, 1)
	decision, err := selector.Select(SelectionRequest{
		Now: now, Candidates: []SelectionCandidate{fast, current},
		CurrentPoolID: "current", CurrentPoolSince: now.Add(-time.Minute),
	})
	if err != nil {
		t.Fatal(err)
	}
	if decision.PoolID != "current" {
		t.Fatalf("pool=%s, want current during dwell", decision.PoolID)
	}
}

func candidate(id string, state RouteState, sampled time.Time, latency time.Duration, capacity, active int) SelectionCandidate {
	return SelectionCandidate{
		Pool: ExecutionPool{ID: id, State: PoolActive}, CapabilityMatched: true,
		Health: RouteHealth{
			PoolID: id, State: state, SuccessRate: 1, ConnectTLSP95: latency,
			FirstByteP95: latency, UploadBytesPerSecond: 10 * 1024 * 1024,
			JitterP95: latency / 10, SampledAt: sampled, ExpiresAt: sampled.Add(45 * time.Second),
		},
		Capacity: CapacitySnapshot{
			PoolID: id, WorkerCount: 1, SafeCapacity: capacity, ActiveTasks: active,
			ResourceHeadroom: 0.8, Stability: 1,
		},
	}
}
