"""Tests para las funciones puras de tee_anthropic_sse.

Cubre split_sse_events y event_to_ndjson sin necesitar mitmproxy.
"""

import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from tee_anthropic_sse import split_sse_events, event_to_ndjson

# ---------------------------------------------------------------------------
# SSE fixture — captura real de la API de Anthropic
# ---------------------------------------------------------------------------

_RAW_SSE = (
    b"event: message_start\n"
    b'data: {"type":"message_start","message":{"model":"claude-opus-4-8","id":"msg_x",'
    b'"type":"message","role":"assistant","content":[],"stop_reason":null}}\n'
    b"\n"
    b"event: content_block_start\n"
    b'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n'
    b"\n"
    b"event: ping\n"
    b'data: {"type": "ping"}\n'
    b"\n"
    b"event: content_block_delta\n"
    b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"P"}}\n'
    b"\n"
    b"event: content_block_delta\n"
    b'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ONG"}}\n'
    b"\n"
    b"event: content_block_stop\n"
    b'data: {"type":"content_block_stop","index":0}\n'
    b"\n"
    b"event: message_delta\n"
    b'data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},'
    b'"usage":{"output_tokens":5}}\n'
    b"\n"
    b"event: message_stop\n"
    b'data: {"type":"message_stop"}\n'
    b"\n"
)


# ---------------------------------------------------------------------------
# split_sse_events
# ---------------------------------------------------------------------------


def test_split_buffer_completo_devuelve_8_bloques():
    """Con el buffer completo devuelve los 8 bloques y leftover vacio."""
    events, leftover = split_sse_events(_RAW_SSE)
    assert len(events) == 8
    assert leftover == b""


def test_split_bloques_contienen_event_y_data():
    """Cada bloque contiene las lineas event: y data: esperadas."""
    events, _ = split_sse_events(_RAW_SSE)
    assert "event: message_start" in events[0]
    assert "event: ping" in events[2]
    assert "event: message_stop" in events[7]


def test_split_buffer_cortado_preserva_incompleto():
    """Con un buffer cortado a la mitad de un evento, devuelve solo los completos."""
    # Encontrar la SEGUNDA aparicion de content_block_delta (quinto evento en total)
    first_occ = _RAW_SSE.find(b"event: content_block_delta\ndata:")
    second_occ = _RAW_SSE.find(b"event: content_block_delta\ndata:", first_occ + 1)
    # Cortar en medio del data: del segundo content_block_delta
    cut_buf = _RAW_SSE[:second_occ + 20]

    events, leftover = split_sse_events(cut_buf)
    # Debe haber exactamente 4 eventos completos:
    # message_start, content_block_start, ping, primer content_block_delta
    assert len(events) == 4
    # El leftover no debe estar vacio (el segundo delta queda a medias)
    assert len(leftover) > 0


def test_split_resto_mas_continuacion_reconstruye_evento():
    """Concatenar leftover + continuacion reconstituye el evento cortado."""
    # Cortar justo antes del \n\n que cierra el primer delta
    cut_point = _RAW_SSE.find(b"\n\nevent: content_block_delta\n", 100)
    first_half = _RAW_SSE[:cut_point + 1]   # termina dentro del separador
    second_half = _RAW_SSE[cut_point + 1:]

    events1, leftover1 = split_sse_events(first_half)
    combined = leftover1 + second_half
    events2, leftover2 = split_sse_events(combined)

    # La union debe cubrir todos los bloques del segundo tramo
    all_events = events1 + events2
    assert len(all_events) == 8
    assert leftover2 == b""


def test_split_buffer_vacio():
    """Buffer vacio devuelve lista vacia y leftover vacio."""
    events, leftover = split_sse_events(b"")
    assert events == []
    assert leftover == b""


def test_split_evento_unico_sin_separador_final():
    """Un evento sin separador final queda como leftover."""
    chunk = b"event: ping\ndata: {\"type\":\"ping\"}"
    events, leftover = split_sse_events(chunk)
    assert events == []
    assert b"ping" in leftover


# ---------------------------------------------------------------------------
# event_to_ndjson
# ---------------------------------------------------------------------------


def test_text_delta_p():
    """content_block_delta text_delta 'P' -> [{type:text_delta, stream_id:1, text:'P'}]."""
    block = (
        "event: content_block_delta\n"
        'data: {"type":"content_block_delta","index":0,'
        '"delta":{"type":"text_delta","text":"P"}}'
    )
    result = event_to_ndjson(block, 1, {})
    assert result == [{"type": "text_delta", "stream_id": 1, "text": "P"}]


def test_text_delta_ong():
    """content_block_delta text_delta 'ONG' -> text 'ONG'."""
    block = (
        "event: content_block_delta\n"
        'data: {"type":"content_block_delta","index":0,'
        '"delta":{"type":"text_delta","text":"ONG"}}'
    )
    result = event_to_ndjson(block, 1, {})
    assert result == [{"type": "text_delta", "stream_id": 1, "text": "ONG"}]


def test_message_stop_con_stop_holder_previo():
    """message_stop con stop_holder ya cargado -> stop_reason end_turn."""
    stop_holder: dict = {}

    # Primero simular message_delta para poblar el holder
    delta_block = (
        "event: message_delta\n"
        'data: {"type":"message_delta","delta":{"stop_reason":"end_turn",'
        '"stop_sequence":null},"usage":{"output_tokens":5}}'
    )
    event_to_ndjson(delta_block, 1, stop_holder)
    assert stop_holder.get("stop_reason") == "end_turn"

    # Ahora message_stop
    stop_block = (
        "event: message_stop\n"
        'data: {"type":"message_stop"}'
    )
    result = event_to_ndjson(stop_block, 1, stop_holder)
    assert result == [{"type": "message_stop", "stream_id": 1, "stop_reason": "end_turn"}]


def test_ping_devuelve_lista_vacia():
    """ping -> []."""
    block = "event: ping\ndata: {\"type\": \"ping\"}"
    result = event_to_ndjson(block, 1, {})
    assert result == []


def test_content_block_start_text_devuelve_vacio():
    """content_block_start para un bloque de texto -> []."""
    block = (
        "event: content_block_start\n"
        'data: {"type":"content_block_start","index":0,'
        '"content_block":{"type":"text","text":""}}'
    )
    result = event_to_ndjson(block, 1, {})
    assert result == []


def test_content_block_start_tool_use():
    """content_block_start tool_use -> tool_use_start con name e id."""
    block = (
        "event: content_block_start\n"
        'data: {"type":"content_block_start","index":1,'
        '"content_block":{"type":"tool_use","id":"toolu_01abc","name":"Bash"}}'
    )
    result = event_to_ndjson(block, 2, {})
    assert result == [
        {
            "type": "tool_use_start",
            "stream_id": 2,
            "tool_name": "Bash",
            "tool_id": "toolu_01abc",
        }
    ]


def test_tool_json_delta():
    """content_block_delta input_json_delta -> tool_json_delta."""
    # Construir el bloque SSE con json.dumps para que el partial_json quede
    # correctamente escapado dentro del JSON del campo data:
    import json as _json
    data_payload = {
        "type": "content_block_delta",
        "index": 1,
        "delta": {
            "type": "input_json_delta",
            "partial_json": '{"command":"ls',
        },
    }
    block = "event: content_block_delta\ndata: " + _json.dumps(data_payload)
    result = event_to_ndjson(block, 3, {})
    assert result == [
        {
            "type": "tool_json_delta",
            "stream_id": 3,
            "partial_json": '{"command":"ls',
        }
    ]


def test_json_invalido_en_data_devuelve_vacio():
    """Linea data: con JSON invalido -> [] (sin excepcion)."""
    block = "event: content_block_delta\ndata: {esto no es json"
    result = event_to_ndjson(block, 1, {})
    assert result == []


def test_bloque_sin_data_devuelve_vacio():
    """Bloque sin linea data: -> []."""
    block = "event: content_block_stop\n"
    result = event_to_ndjson(block, 1, {})
    assert result == []


# ---------------------------------------------------------------------------
# Integración del parseo: secuencia completa produce PONG + message_stop
# ---------------------------------------------------------------------------


def test_integracion_secuencia_completa_produce_pong_y_stop():
    """Los 8 bloques en orden producen text_delta 'P'+'ONG' y un message_stop end_turn."""
    events, leftover = split_sse_events(_RAW_SSE)
    assert leftover == b""

    stop_holder: dict = {}
    all_ndjson: list[dict] = []
    for block in events:
        all_ndjson.extend(event_to_ndjson(block, 1, stop_holder))

    text_deltas = [o for o in all_ndjson if o["type"] == "text_delta"]
    message_stops = [o for o in all_ndjson if o["type"] == "message_stop"]

    concatenated = "".join(d["text"] for d in text_deltas)
    assert concatenated == "PONG"

    assert len(message_stops) == 1
    assert message_stops[0]["stop_reason"] == "end_turn"
    assert message_stops[0]["stream_id"] == 1


def test_integracion_stream_id_se_propaga():
    """stream_id se propaga correctamente a todos los eventos emitidos."""
    events, _ = split_sse_events(_RAW_SSE)
    stop_holder: dict = {}
    for block in events:
        for obj in event_to_ndjson(block, 42, stop_holder):
            assert obj["stream_id"] == 42


def test_integracion_determinismo():
    """Parsear el mismo buffer dos veces produce exactamente el mismo resultado."""
    def parse_all(stream_id: int) -> list[dict]:
        evs, _ = split_sse_events(_RAW_SSE)
        holder: dict = {}
        result: list[dict] = []
        for b in evs:
            result.extend(event_to_ndjson(b, stream_id, holder))
        return result

    assert parse_all(1) == parse_all(1)
