"""Test anti-starvation fairness: low-priority tasks get minimum execution ratio even under high-priority load."""

import pytest
import time
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from feid.agent.maestro import AgenteMaestroNASA
from feid.agent.factory import AgenteFactory


class StressAgent(AgenteMaestroNASA):
    """Agent for anti-starvation stress testing."""
    def _technical_work(self, task):
        time.sleep(0.01)  # Simulate quick work
        return f"Done: {task}"


def test_anti_starvation_ratio_maintained():
    """
    Validate that mixed-priority load still executes some low-priority tasks.

    IMPORTANT NOTE: PriorityQueue + strict fairness have a fundamental tension:
    - Strict priority ordering pushes low-priority items to the back
    - Anti-starvation tries to mitigate by scanning for low-priority tasks
    - Effectiveness depends on low-priority availability in the queue

    This test validates:
    1) The mechanism detects when low-priority execution is too low
    2) _find_low_priority_task() works when items are available
    """
    agent = AgenteFactory.industrial(StressAgent, "test_starvation")

    # Disable rate limiting impact for fairness tests
    agent.configure_rate_limit(10000.0, 10000.0)
    agent.add_origin_limit("default", 10000.0, 10000.0)

    # Ensure anti-starvation is enabled
    assert agent.anti_starvation_manager is not None, "Anti-starvation must be enabled in Industrial"

    # Send a balanced mix of priorities
    num_high = 50
    num_low = 10

    print(f"\n[TEST] Sending {num_high} high-prio + {num_low} low-prio (balanced)...")

    # High-priority (0-1)
    for i in range(num_high):
        agent.send_task(f"HighPrio-{i}", priority=0 if i % 2 == 0 else 1)

    # Low-priority (4-5)
    for i in range(num_low):
        agent.send_task(f"LowPrio-{i}", priority=4 if i % 2 == 0 else 5)

    print(f"[TEST] Initial queue size: {agent.inbox.qsize()} tasks")

    # Run until the queue is drained (with timeout)
    start = time.time()
    timeout = 30

    while not agent.inbox.empty() and (time.time() - start) < timeout:
        time.sleep(0.1)

    elapsed = time.time() - start
    print(f"[TEST] Completed in {elapsed:.2f}s")

    # Anti-starvation stats
    stats = agent.get_anti_starvation_stats()
    print(f"[TEST] Stats: {stats}")

    total_executed = stats.get("total_tareas", 0)
    low_executed = stats.get("tareas_baja_prioridad", 0)
    ratio_actual = stats.get("ratio_actual", 0.0)
    ratio_minimo = stats.get("ratio_minimo_requerido", 0.0)

    print(f"[TEST] Total executed: {total_executed}, Low-prio: {low_executed}, Ratio: {ratio_actual:.2%}")
    print(f"[TEST] Required ratio: {ratio_minimo:.2%}")
    print(f"[TEST] Manager total: {agent.anti_starvation_manager.contador_tareas}")
    print(f"[TEST] Manager low-prio: {agent.anti_starvation_manager.tareas_baja_prioridad_ejecutadas}")

    # With 50 high / 10 low = 16.7% low ideally
    # Due to PriorityQueue ordering, expect > 5% at minimum
    min_expected_ratio = 0.05
    if total_executed >= 10:
        assert ratio_actual >= min_expected_ratio, \
            f"Ratio {ratio_actual:.2%} too low (expected >= {min_expected_ratio:.2%})"
        print(f"[TEST] >>> Anti-starvation detection working: {ratio_actual:.2%} >= {min_expected_ratio:.2%}")

    agent.graceful_shutdown()


def test_anti_starvation_sustained_high_load():
    """Validate fairness under sustained high-priority load with enough low-priority tasks."""
    agent = AgenteFactory.industrial(StressAgent, "test_starvation_sustained")

    # Disable rate limiting impact for fairness tests
    agent.configure_rate_limit(10000.0, 10000.0)
    agent.add_origin_limit("default", 10000.0, 10000.0)

    assert agent.anti_starvation_manager is not None, "Anti-starvation must be enabled in Industrial"

    # Reset counters to isolate this run
    agent.clear_anti_starvation_stats()

    num_high = 200
    num_low = 50
    expected_total = num_high + num_low

    # Enqueue low-priority tasks first to ensure availability during the run
    for i in range(num_low):
        agent.send_task(f"LowSustained-{i}", priority=4)

    # Sustained high-priority load
    for i in range(num_high):
        agent.send_task(f"HighSustained-{i}", priority=0)

    start = time.time()
    timeout = 30

    while (time.time() - start) < timeout:
        stats = agent.get_anti_starvation_stats()
        total_executed = stats.get("total_tareas", 0)
        if total_executed >= expected_total and agent.inbox.empty():
            break
        time.sleep(0.1)

    stats = agent.get_anti_starvation_stats()
    total_executed = stats.get("total_tareas", 0)
    low_executed = stats.get("tareas_baja_prioridad", 0)
    ratio_actual = stats.get("ratio_actual", 0.0)

    assert total_executed >= expected_total, (
        f"Expected at least {expected_total} tasks executed, got {total_executed}"
    )
    assert low_executed >= num_low, (
        f"Expected at least {num_low} low-priority tasks executed, got {low_executed}"
    )

    # Allow slight drift due to timing/ordering, but keep fairness near the configured ratio.
    assert ratio_actual >= 0.18, f"Low-priority ratio too low: {ratio_actual:.2%}"

    agent.graceful_shutdown()


def test_anti_starvation_disabled_on_quick():
    """Validate that Quick flavor has anti-starvation disabled."""
    agent = AgenteFactory.quick(StressAgent, "test_quick")

    assert agent.anti_starvation_manager is None, "Quick flavor must not enable anti-starvation"
    print("[TEST] >>> Quick flavor correctly disables anti-starvation")

    agent.graceful_shutdown()


def test_anti_starvation_configurable():
    """Validate that the minimum ratio can be changed at runtime."""
    agent = AgenteFactory.standard(StressAgent, "test_config")

    assert agent.anti_starvation_manager is not None

    original_ratio = agent.anti_starvation_manager.ratio_minimo
    print(f"[TEST] Original ratio: {original_ratio:.2%}")

    agent.set_min_low_priority_ratio(0.3)
    new_ratio = agent.anti_starvation_manager.ratio_minimo

    assert new_ratio == 0.3, f"Ratio should be 0.3, got {new_ratio}"
    print(f"[TEST] >>> Ratio changed to {new_ratio:.2%}")

    agent.graceful_shutdown()


def test_find_low_priority_task():
    """
    Validate that _find_low_priority_task extracts a low-priority task correctly.

    NOTE: The test stops the agent before touching the queue to avoid races.
    """
    agent = AgenteFactory.standard(StressAgent, "test_find")

    # Fully stop the agent
    agent.alive = False
    agent.accepting_tasks = False
    import time
    import threading

    # Wait for the worker thread to stop
    max_wait = 0
    if agent.worker_thread and agent.worker_thread.is_alive():
        print("[TEST] Worker thread is alive, waiting for it to stop...")
        while agent.worker_thread.is_alive() and max_wait < 5:
            time.sleep(0.1)
            max_wait += 0.1
        if agent.worker_thread.is_alive():
            print("[TEST] Warning: Worker thread still alive after 5s")

    # Replace the inbox to avoid races with previous threads
    from queue import PriorityQueue
    agent.inbox = PriorityQueue()
    print("[TEST] Replaced inbox with fresh PriorityQueue")

    # Enqueue tasks directly
    agent.inbox.put((0, time.time(), 300, 0, 0, "high1", "high1", "json", None))
    agent.inbox.put((1, time.time(), 300, 0, 0, "high2", "high2", "json", None))
    agent.inbox.put((4, time.time(), 300, 0, 0, "low1", "low1", "json", None))
    agent.inbox.put((0, time.time(), 300, 0, 0, "high3", "high3", "json", None))
    agent.inbox.put((5, time.time(), 300, 0, 0, "low2", "low2", "json", None))

    print(f"[TEST] Queue size after put x5: {agent.inbox.qsize()} tasks")

    low_task = agent._find_low_priority_task()
    print(f"[TEST] Found low-prio: {low_task[0] if low_task else None}")
    assert low_task is not None, "Should find a low-priority task"

    prio = low_task[0]
    assert prio >= 3, f"Priority {prio} is not low-priority"

    remaining = agent.inbox.qsize()
    print(f"[TEST] Remaining after _find_low_priority_task: {remaining}")
    assert remaining == 4, f"Expected 4 remaining tasks, got {remaining}"

    print(f"[TEST] >>> Found low-prio (prio={prio}), {remaining} remaining")


if __name__ == "__main__":
    pytest.main([__file__, "-v", "-s"])
