import time
import types

import pytest

from feid.agent import AgenteMaestroNASA
from feid.core import ConfigAgente, MiddlewarePlugin
import feid.agent.maestro as maestro_module


class DummyThread:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def start(self):
        return None


class ImmediateThread:
    """Thread que ejecuta el target inmediatamente."""
    def __init__(self, target=None, args=(), daemon=None):
        self.target = target
        self.args = args

    def start(self):
        if self.target:
            self.target(*self.args)


class AgenteDummy(AgenteMaestroNASA):
    def _technical_work(self, task):
        return f"ok:{task}"

    def perceive_environment(self):
        return None


@pytest.fixture
def agente(monkeypatch):
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0)
    return AgenteDummy("DUMMY", config=config, auto_persist=False)


def test_recibir_mensaje_simple(agente):
    m_id = agente.receive_message("hola", priority=1, complexity=3, ttl=10)
    assert m_id is not None
    assert agente.inbox.qsize() == 1


def test_recibir_mensaje_fipa(agente):
    mensaje = {"performative": "request", "content": "do_x", "conversation-id": "c1"}
    m_id = agente.receive_message(mensaje)
    assert m_id == "c1"
    prio, _, _, _, _, mid, tarea, proto, corr_id = agente.inbox.get()
    assert mid == "c1"
    assert proto == "FIPA"
    assert tarea.startswith("[REQUEST]")
    assert corr_id is not None


def test_recibir_mensaje_kqml(agente):
    mensaje = {"verb": "ask-one", "content": "query", "reply-with": "r1"}
    m_id = agente.receive_message(mensaje)
    assert m_id == "r1"
    _, _, _, _, _, mid, _, proto, corr_id = agente.inbox.get()
    assert mid == "r1"
    assert proto == "KQML"
    assert corr_id is not None


def test_recibir_mensaje_jsonrpc(agente):
    mensaje = {"jsonrpc": "2.0", "id": 42, "method": "m", "params": {"a": 1}}
    m_id = agente.receive_message(mensaje)
    assert m_id == "42"
    _, _, _, _, _, mid, tarea, proto, corr_id = agente.inbox.get()
    assert mid == "42"
    assert proto == "JSON-RPC"
    assert "METHOD" in tarea
    assert corr_id is not None


def test_enviar_orden_saturacion(agente):
    agente.capacity = 0
    m_id = agente.send_task("x")
    assert m_id is None
    assert len(agente.incident_log) == 1


def test_enviar_orden_seguridad(agente):
    m_id = agente.send_task("shutdown now", complexity=9)
    assert m_id is None
    assert len(agente.incident_log) == 1


def test_crear_recibo_poliglota(agente):
    r = agente._create_polyglot_receipt("m1", "COMPLETE", "ok", time.time(), time.time(), "FIPA")
    assert r["performative"] == "inform"
    assert r["conversation-id"] == "m1"


def test_registrar_incidente_rechazados(agente):
    agente._register_incident("SATURATION", "tarea", "causa", "m1")
    assert agente.global_metrics["rejected"] == 1


def test_obtener_detalle_misiones_cola(agente):
    agente.send_task("t1")
    detalle = agente.get_mission_details("queue")
    # Los elementos de la cola son ahora el último elemento (tarea, que es una cadena)
    assert len(detalle) > 0


def test_obtener_detalle_misiones_limit(agente):
    agente._dispatch_event("SUCCESS", {"m": 1})
    agente._dispatch_event("SUCCESS", {"m": 2})
    detalle = agente.get_mission_details("successes", limit=1)
    assert len(detalle) == 1
    assert detalle[0]["m"] == 2


def test_dead_letter_en_fallo_fatal(monkeypatch):
    class AgenteFatal(AgenteMaestroNASA):
        def _technical_work(self, task):
            raise RuntimeError("fatal")

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_reintentos=0, max_workers=0)
    agente = AgenteFatal("F", config=config, auto_persist=False)

    original_despachar = agente._dispatch_event

    def despachar_y_detener(tipo, datos):
        original_despachar(tipo, datos)
        agente.alive = False

    monkeypatch.setattr(agente, "_dispatch_event", despachar_y_detener)
    agente.inbox.put((1, time.time(), 10, 5, 0, "m1", "t", "SIMPLE", "corr-1"))

    agente._operational_cycle()
    assert len(agente.dead_letter_queue) == 1
    assert agente.dead_letter_queue[0]["status"] == "ERROR_FATAL"


def test_cancelar_tarea(agente):
    m1 = agente.send_task("t1")
    m2 = agente.send_task("t2")
    assert agente.cancel_task(m1) is True
    ids = [item[5] for item in list(agente.inbox.queue)]
    assert m1 not in ids
    assert m2 in ids
    assert agente.cancel_task("no-existe") is False


def test_ttl_expirado_incrementa_metrica(monkeypatch):
    class AgenteTTL(AgenteMaestroNASA):
        def _technical_work(self, task):
            return "ok"

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0)
    agente = AgenteTTL("TTL", config=config, auto_persist=False)
    agente.alive = True

    def registrar_incidente_stub(*args, **kwargs):
        agente.alive = False

    monkeypatch.setattr(agente, "_register_incident", registrar_incidente_stub)
    agente.inbox.put((1, time.time() - 10, 1, 1, 0, "m1", "t", "SIMPLE", "corr-1"))

    agente._operational_cycle()
    assert agente.global_metrics["ttl_deaths"] == 1


def test_external_handler_invocado(monkeypatch):
    llamadas = []

    def handler(tipo, datos):
        llamadas.append((tipo, datos))

    class AgenteHandler(AgenteMaestroNASA):
        def _technical_work(self, task):
            return "ok"

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0)
    agente = AgenteHandler(
        "H", config=config, handler=handler, auto_persist=False
    )
    monkeypatch.setattr(maestro_module.threading, "Thread", ImmediateThread)
    agente._dispatch_event("SUCCESS", {"x": 1})

    assert llamadas and llamadas[0][0] == "SUCCESS"


def test_reintento_reencola_tarea(monkeypatch):
    class AgenteRetry(AgenteMaestroNASA):
        def _technical_work(self, task):
            raise RuntimeError("fail")

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_reintentos=3, max_workers=0)
    agente = AgenteRetry(
        "R", config=config, auto_persist=False
    )
    monkeypatch.setattr(maestro_module.threading, "Thread", ImmediateThread)
    monkeypatch.setattr(maestro_module.time, "sleep", lambda *_: None)

    tarea = (1, time.time(), 10, 5, 0, "m1", "t", "SIMPLE", "corr-1")
    llamadas = {"count": 0}

    def get_side_effect(*_args, **_kwargs):
        llamadas["count"] += 1
        if llamadas["count"] == 1:
            agente.alive = False
            return tarea
        raise maestro_module.Empty()

    monkeypatch.setattr(agente.inbox, "get", get_side_effect)
    agente._operational_cycle()

    assert agente.global_metrics["recovered_failures"] == 1
    reencoladas = list(agente.inbox.queue)
    assert reencoladas and reencoladas[0][4] == 1


def test_soft_timeout_envia_dlq(monkeypatch):
    class AgenteTimeout(AgenteMaestroNASA):
        def _technical_work(self, task):
            time.sleep(0.05)
            return "ok"

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_reintentos=0, max_workers=0, task_timeout_seconds=0.01)
    agente = AgenteTimeout("T", config=config, auto_persist=False)

    original_despachar = agente._dispatch_event

    def despachar_y_detener(tipo, datos):
        original_despachar(tipo, datos)
        agente.alive = False

    monkeypatch.setattr(agente, "_dispatch_event", despachar_y_detener)
    agente.inbox.put((1, time.time(), 10, 5, 0, "m1", "t", "SIMPLE", "corr-1"))

    agente._operational_cycle()
    assert len(agente.dead_letter_queue) == 1
    assert agente.dead_letter_queue[0]["status"] == "ERROR_FATAL"


def test_result_truncation_in_receipt(monkeypatch):
    class AgenteBigResult(AgenteMaestroNASA):
        def _technical_work(self, task):
            return "X" * 2048

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0, max_result_bytes=128, payload_overflow_policy="truncate")
    agente = AgenteBigResult("R", config=config, auto_persist=False)

    original_despachar = agente._dispatch_event

    def despachar_y_detener(tipo, datos):
        original_despachar(tipo, datos)
        agente.alive = False

    monkeypatch.setattr(agente, "_dispatch_event", despachar_y_detener)
    agente.inbox.put((1, time.time(), 10, 5, 0, "m1", "t", "SIMPLE", "corr-1"))

    agente._operational_cycle()
    assert len(agente.outbox) == 1
    receipt = agente.outbox[-1]
    assert receipt["status"] == "COMPLETE"
    assert receipt["result"]["_truncated"] is True
    assert "hash" in receipt["result"]


def test_payload_limit_rejects(monkeypatch):
    class AgentePayload(AgenteMaestroNASA):
        def _technical_work(self, task):
            return "ok"

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0, max_task_bytes=32)
    agente = AgentePayload("P", config=config, auto_persist=False)

    big_payload = "X" * 256
    meta = agente.send_task(big_payload, return_metadata=True)
    assert meta["accepted"] is False
    assert meta["reason"] == "PAYLOAD_INVALID"


def test_middleware_on_error_and_hook(monkeypatch):
    class ErrorMiddleware(MiddlewarePlugin):
        def __init__(self):
            self.called = False

        def on_error(self, error, context):
            self.called = True
            return error

    class AgenteFail(AgenteMaestroNASA):
        def _technical_work(self, task):
            raise ValueError("boom")

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_reintentos=0, max_workers=0)
    agente = AgenteFail("F", config=config, auto_persist=False)

    middleware = ErrorMiddleware()
    agente.middleware_stack.add(middleware)

    hook_calls = []

    def hook(payload):
        hook_calls.append(payload)

    agente.lifecycle_hooks.registrar("on_task_failed", hook)

    original_despachar = agente._dispatch_event

    def despachar_y_detener(tipo, datos):
        original_despachar(tipo, datos)
        agente.alive = False

    monkeypatch.setattr(agente, "_dispatch_event", despachar_y_detener)
    agente.inbox.put((1, time.time(), 10, 5, 0, "m1", "t", "SIMPLE", "corr-1"))

    agente._operational_cycle()
    assert middleware.called is True
    assert hook_calls and hook_calls[0]["error_type"] == "ValueError"


def test_hook_on_task_received_invoked(monkeypatch):
    class AgenteHook(AgenteMaestroNASA):
        def _technical_work(self, task):
            return "ok"

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0)
    agente = AgenteHook("H1", config=config, auto_persist=False)

    calls = []

    def hook(payload):
        calls.append(payload)

    agente.lifecycle_hooks.registrar("on_task_received", hook)

    task_id = agente.send_task("hola")
    assert task_id is not None
    assert calls and calls[0]["task_id"] == task_id
    assert calls[0]["task"] == "hola"


def test_hook_on_task_completed_invoked(monkeypatch):
    class AgenteOk(AgenteMaestroNASA):
        def _technical_work(self, task):
            return "ok"

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_reintentos=0, max_workers=0)
    agente = AgenteOk("H2", config=config, auto_persist=False)

    calls = []

    def hook(payload):
        calls.append(payload)

    agente.lifecycle_hooks.registrar("on_task_completed", hook)

    original_despachar = agente._dispatch_event

    def despachar_y_detener(tipo, datos):
        original_despachar(tipo, datos)
        agente.alive = False

    monkeypatch.setattr(agente, "_dispatch_event", despachar_y_detener)
    agente.inbox.put((1, time.time(), 10, 5, 0, "m1", "t", "SIMPLE", "corr-1"))

    agente._operational_cycle()
    assert calls and calls[0]["status"] == "COMPLETE"


def test_find_low_priority_task_preserves_heap(monkeypatch):
    class AgenteQ(AgenteMaestroNASA):
        def _technical_work(self, task):
            return "ok"

    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0)
    agente = AgenteQ("Q", config=config, auto_persist=False)

    items = [
        (0, time.time(), 10, 1, 0, "m1", "t1", "SIMPLE", "c1"),
        (4, time.time(), 10, 1, 0, "m2", "t2", "SIMPLE", "c2"),
        (2, time.time(), 10, 1, 0, "m3", "t3", "SIMPLE", "c3"),
        (5, time.time(), 10, 1, 0, "m4", "t4", "SIMPLE", "c4"),
    ]
    for item in items:
        agente.inbox.put(item)

    original_ids = [i[5] for i in list(agente.inbox.queue)]
    low = agente._find_low_priority_task()

    assert low is not None
    assert low[0] >= 3

    remaining_ids = [i[5] for i in list(agente.inbox.queue)]
    assert len(remaining_ids) == len(original_ids) - 1
    assert low[5] not in remaining_ids

    # Heap property check
    q = list(agente.inbox.queue)
    for idx, item in enumerate(q):
        left = 2 * idx + 1
        right = 2 * idx + 2
        if left < len(q):
            assert item <= q[left]
        if right < len(q):
            assert item <= q[right]


# ============================================================================
# ENTERPRISE FEATURES TESTS
# ============================================================================

def test_correlation_id_tracking(agente):
    """Test que correlation IDs se crean y rastrean correctamente."""
    m_id1 = agente.send_task("tarea1", m_id="t1", correlation_id="corr-123")
    m_id2 = agente.send_task("tarea2", m_id="t2", correlation_id="corr-123")
    
    # Ambas tareas deben estar en la cola
    assert agente.inbox.qsize() == 2
    
    # Obtener metadata de correlación
    meta = agente.get_correlation("corr-123")
    assert "created_at" in meta
    assert meta["hop_count"] >= 0
    assert "chain" in meta


def test_correlation_id_in_receipt(agente, monkeypatch):
    """Test que correlation ID aparece en los recibos."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    config = ConfigAgente(max_workers=0)
    agente = AgenteDummy("D", config=config, auto_persist=False)
    monkeypatch.setattr(maestro_module.threading, "Thread", ImmediateThread)
    
    # Enviar con correlation_id explícito
    m_id = agente.send_task("test_task", correlation_id="trace-xyz")
    agente.alive = False
    agente._operational_cycle()
    
    # Procesar una tarea y verificar que el recibo contiene correlation_id
    recibos = agente.outbox
    if recibos:
        recibo = list(recibos)[0]
        assert "correlation_id" in recibo


def test_circuit_breaker_closed_state(agente):
    """Test que el circuit breaker inicia en estado CLOSED."""
    assert agente.get_circuit_state() == "closed"


def test_circuit_breaker_opens_on_failures(monkeypatch):
    """Test que el circuit breaker se abre después de fallos consecutivos."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    
    class AgenteQueFalla(AgenteMaestroNASA):
        def _technical_work(self, task):
            raise ValueError("Fallo intencional")
        
        def perceive_environment(self):
            return None
    
    config = ConfigAgente(max_workers=0, circuit_breaker_threshold=3)
    agente = AgenteQueFalla(
        "FAIL", 
        config=config,
        auto_persist=False
    )
    monkeypatch.setattr(maestro_module.threading, "Thread", ImmediateThread)
    monkeypatch.setattr(maestro_module.time, "sleep", lambda *_: None)
    
    # Simular 3 fallos consecutivos
    fallos = 0
    for i in range(3):
        try:
            agente.circuit_breaker.call(lambda: 1/0)  # TypeError
        except:
            fallos += 1
    
    # Después de 3 fallos, el circuito debe estar OPEN
    assert agente.get_circuit_state() == "open"
    assert fallos == 3


def test_circuit_breaker_half_open_after_timeout(monkeypatch):
    """Test transición de OPEN a HALF_OPEN después del timeout."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    
    config = ConfigAgente(max_workers=0, circuit_breaker_threshold=2)
    agente = AgenteDummy("CB_HO", config=config, auto_persist=False)
    
    # Forzar failures
    for _ in range(2):
        try:
            agente.circuit_breaker.call(lambda: 1/0)
        except:
            pass
    
    assert agente.get_circuit_state() == "open"
    
    # Modificar tiempo de último fallo para simular timeout
    agente.circuit_breaker.last_failure_time = time.time() - 100
    
    # Intentar llamada debe pasar a HALF_OPEN
    try:
        agente.circuit_breaker.call(lambda: "success")
    except:
        pass
    
    assert agente.get_circuit_state() in ["half_open", "closed"]


def test_retry_strategy_exponential_backoff(monkeypatch):
    """Test que ExponentialBackoffStrategy genera delays exponenciales."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    
    strategy = maestro_module.ExponentialBackoffStrategy(base_delay=1.0, max_delay=60.0, jitter=False)
    
    # Sin jitter, delays deben ser potencias de 2
    delay_0 = strategy.get_delay(0, 5)
    delay_1 = strategy.get_delay(1, 5)
    delay_2 = strategy.get_delay(2, 5)
    
    assert delay_0 == 1.0  # 2^0
    assert delay_1 == 2.0  # 2^1
    assert delay_2 == 4.0  # 2^2
    
    # Verificar capping
    delay_max = strategy.get_delay(10, 5)
    assert delay_max == 60.0


def test_retry_strategy_linear_backoff(monkeypatch):
    """Test que LinearBackoffStrategy genera delays lineales."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    from feid.enterprise import LinearBackoffStrategy
    
    strategy = LinearBackoffStrategy(increment=0.5, max_delay=10.0)
    
    delay_0 = strategy.get_delay(0, 5)
    delay_1 = strategy.get_delay(1, 5)
    delay_2 = strategy.get_delay(2, 5)
    
    assert delay_0 == 0.5
    assert delay_1 == 1.0
    assert delay_2 == 1.5


def test_retry_strategy_fixed_delay(monkeypatch):
    """Test que FixedDelayStrategy produce delay constante."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    from feid.enterprise import FixedDelayStrategy
    
    strategy = FixedDelayStrategy(delay=2.5)
    
    assert strategy.get_delay(0, 5) == 2.5
    assert strategy.get_delay(1, 5) == 2.5
    assert strategy.get_delay(10, 5) == 2.5


def test_agente_with_custom_retry_strategy(monkeypatch):
    """Test que el agente puede usar estrategia de reintentos personalizada."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    from feid.enterprise import LinearBackoffStrategy
    
    custom_strategy = LinearBackoffStrategy(increment=0.1)
    config = ConfigAgente(max_workers=0)
    agente = AgenteDummy(
        "CUSTOM_RETRY",
        config=config,
        auto_persist=False,
        retry=custom_strategy
    )
    
    assert agente.get_retry_strategy() == "LinearBackoffStrategy"


def test_enterprise_monitoring_health(agente):
    """Test que monitorear_salud incluye información de enterprise features."""
    salud = agente.monitor_health()
    
    assert "circuit_breaker" in salud
    assert "retry_strategy" in salud
    assert salud["circuit_breaker"] == "closed"
    assert "Exponential" in salud["retry_strategy"]


def test_correlation_id_with_delegation(monkeypatch):
    """Test que correlation ID se propaga en delegaciones."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    
    config = ConfigAgente(max_workers=0)
    agente1 = AgenteDummy("A1", config=config, auto_persist=False)
    agente2 = AgenteDummy("A2", config=config, auto_persist=False)
    
    # Agente 1 delega tarea a agente 2 con correlation_id
    corr_id = agente1.correlation_tracker.create_correlation("delegation-1")
    m_id = agente2.send_task("delegated_task", correlation_id=corr_id)
    
    # Ambos agentes deben tener registro de la correlación
    meta1 = agente1.get_correlation(corr_id)
    meta2 = agente2.get_correlation(corr_id)
    
    assert meta1 or meta2  # Al menos uno debe tener metadata
    

def test_circuit_breaker_success_closes_half_open(monkeypatch):
    """Test que éxito en HALF_OPEN devuelve a CLOSED."""
    monkeypatch.setattr(maestro_module.threading, "Thread", DummyThread)
    
    config = ConfigAgente(max_workers=0, circuit_breaker_threshold=2)
    agente = AgenteDummy("CB_CLOSE", config=config, auto_persist=False)
    
    # Forzar apertura
    for _ in range(2):
        try:
            agente.circuit_breaker.call(lambda: 1/0)
        except:
            pass
    
    assert agente.get_circuit_state() == "open"
    
    # Simular timeout y luego éxito
    agente.circuit_breaker.last_failure_time = time.time() - 100
    try:
        result = agente.circuit_breaker.call(lambda: "ok")
        assert result == "ok"
    except:
        pass
    
    # Después de éxito en HALF_OPEN, debe estar CLOSED
    assert agente.get_circuit_state() == "closed"

