"""Tests for CustomTransformer, UpdatesTransformer, CheckpointsTransformer, DebugTransformer, TasksTransformer.

These transformers capture raw protocol events for their respective stream
modes and expose them as native projections on the run stream (run.custom,
run.updates, run.checkpoints, run.debug, run.tasks). Tests dispatch synthetic
protocol events through a StreamMux to isolate transformer logic; the final
group exercises real graphs through stream_events(version="v3").
"""

from __future__ import annotations

import operator
import time
from typing import Annotated, Any

from typing_extensions import TypedDict

from langgraph.constants import END, START
from langgraph.graph import StateGraph
from langgraph.stream._mux import StreamMux
from langgraph.stream.stream_channel import StreamChannel
from langgraph.stream.transformers import (
    CheckpointsTransformer,
    CustomTransformer,
    DebugTransformer,
    LifecycleTransformer,
    TasksTransformer,
    UpdatesTransformer,
)

TS = int(time.time() * 1000)


def _custom_event(namespace: list[str], data: Any) -> dict[str, Any]:
    return {
        "type": "event",
        "method": "custom",
        "params": {"namespace": namespace, "timestamp": TS, "data": data},
    }


def _checkpoints_event(namespace: list[str], data: Any) -> dict[str, Any]:
    return {
        "type": "event",
        "method": "checkpoints",
        "params": {"namespace": namespace, "timestamp": TS, "data": data},
    }


def _debug_event(namespace: list[str], data: Any) -> dict[str, Any]:
    return {
        "type": "event",
        "method": "debug",
        "params": {"namespace": namespace, "timestamp": TS, "data": data},
    }


def _tasks_event(namespace: list[str], data: Any) -> dict[str, Any]:
    return {
        "type": "event",
        "method": "tasks",
        "params": {"namespace": namespace, "timestamp": TS, "data": data},
    }


def _updates_event(namespace: list[str], data: Any) -> dict[str, Any]:
    return {
        "type": "event",
        "method": "updates",
        "params": {"namespace": namespace, "timestamp": TS, "data": data},
    }


def _arm(mux: StreamMux, transformer: Any) -> None:
    """Force projection logs to accept pushes (skip lazy-subscribe gate)."""
    mux._events._subscribed = True
    transformer._log._subscribed = True


def _unstamped(items):
    """Strip push stamps from a StreamChannel's internal buffer."""
    return [item for _stamp, item in items]


def _drain(transformer: Any) -> list[Any]:
    return _unstamped(transformer._log._items)


# ---------------------------------------------------------------------------
# CustomTransformer
# ---------------------------------------------------------------------------


def test_custom_captures_root_scope_events() -> None:
    t = CustomTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_custom_event([], {"status": "processing"}))
    mux.push(_custom_event([], {"status": "done"}))

    items = _drain(t)
    assert items == [{"status": "processing"}, {"status": "done"}]


def test_custom_ignores_subgraph_scope_events() -> None:
    t = CustomTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_custom_event(["subgraph:abc"], {"from": "child"}))

    assert _drain(t) == []


def test_custom_scoped_transformer_captures_own_scope() -> None:
    t = CustomTransformer(scope=("agent:abc",))
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_custom_event([], {"from": "root"}))
    mux.push(_custom_event(["agent:abc"], {"from": "self"}))
    mux.push(_custom_event(["agent:abc", "deep:def"], {"from": "child"}))

    items = _drain(t)
    assert items == [{"from": "self"}]


def test_custom_preserves_any_payload_type() -> None:
    t = CustomTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_custom_event([], "string_payload"))
    mux.push(_custom_event([], 42))
    mux.push(_custom_event([], [1, 2, 3]))

    assert _drain(t) == ["string_payload", 42, [1, 2, 3]]


def test_custom_does_not_suppress_from_main_log() -> None:
    t = CustomTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_custom_event([], "data"))

    methods = [evt["method"] for evt in _unstamped(mux._events._items)]
    assert "custom" in methods


def test_custom_ignores_other_methods() -> None:
    t = CustomTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(
        {
            "type": "event",
            "method": "values",
            "params": {"namespace": [], "timestamp": TS, "data": {}},
        }
    )
    assert _drain(t) == []


def test_custom_required_stream_modes() -> None:
    assert CustomTransformer.required_stream_modes == ("custom",)


def test_custom_is_native() -> None:
    assert getattr(CustomTransformer, "_native", False) is True


def test_custom_init_returns_correct_key() -> None:
    t = CustomTransformer()
    projection = t.init()
    assert "custom" in projection
    assert isinstance(projection["custom"], StreamChannel)


# ---------------------------------------------------------------------------
# CheckpointsTransformer
# ---------------------------------------------------------------------------


def test_checkpoints_captures_root_scope_events() -> None:
    t = CheckpointsTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    checkpoint_data = {"values": {"x": 1}, "next": ["node_b"]}
    mux.push(_checkpoints_event([], checkpoint_data))

    items = _drain(t)
    assert items == [checkpoint_data]


def test_checkpoints_ignores_subgraph_events() -> None:
    t = CheckpointsTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_checkpoints_event(["child:abc"], {"values": {"x": 1}}))

    assert _drain(t) == []


def test_checkpoints_scoped_transformer() -> None:
    t = CheckpointsTransformer(scope=("sub:abc",))
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_checkpoints_event([], {"from": "root"}))
    mux.push(_checkpoints_event(["sub:abc"], {"from": "self"}))

    assert _drain(t) == [{"from": "self"}]


def test_checkpoints_does_not_suppress_from_main_log() -> None:
    t = CheckpointsTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_checkpoints_event([], {"values": {}}))

    methods = [evt["method"] for evt in _unstamped(mux._events._items)]
    assert "checkpoints" in methods


def test_checkpoints_required_stream_modes() -> None:
    assert CheckpointsTransformer.required_stream_modes == ("checkpoints",)


def test_checkpoints_is_native() -> None:
    assert getattr(CheckpointsTransformer, "_native", False) is True


# ---------------------------------------------------------------------------
# DebugTransformer
# ---------------------------------------------------------------------------


def test_debug_captures_root_scope_events() -> None:
    t = DebugTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    debug_data = {
        "step": 0,
        "type": "checkpoint",
        "timestamp": "2026-01-01T00:00:00Z",
        "payload": {"values": {"x": 1}},
    }
    mux.push(_debug_event([], debug_data))

    items = _drain(t)
    assert items == [debug_data]


def test_debug_ignores_subgraph_events() -> None:
    t = DebugTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_debug_event(["child:abc"], {"step": 0, "type": "task"}))

    assert _drain(t) == []


def test_debug_captures_multiple_event_types() -> None:
    t = DebugTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_debug_event([], {"step": 0, "type": "checkpoint", "payload": {}}))
    mux.push(_debug_event([], {"step": 1, "type": "task", "payload": {}}))
    mux.push(_debug_event([], {"step": 1, "type": "task_result", "payload": {}}))

    items = _drain(t)
    assert len(items) == 3
    assert [d["type"] for d in items] == ["checkpoint", "task", "task_result"]


def test_debug_does_not_suppress_from_main_log() -> None:
    t = DebugTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_debug_event([], {"step": 0}))

    methods = [evt["method"] for evt in _unstamped(mux._events._items)]
    assert "debug" in methods


def test_debug_required_stream_modes() -> None:
    assert DebugTransformer.required_stream_modes == ("debug",)


def test_debug_is_native() -> None:
    assert getattr(DebugTransformer, "_native", False) is True


# ---------------------------------------------------------------------------
# TasksTransformer
# ---------------------------------------------------------------------------


def test_tasks_captures_root_scope_events() -> None:
    t = TasksTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    task_start = {"id": "t1", "name": "my_node", "input": None, "triggers": []}
    mux.push(_tasks_event([], task_start))

    items = _drain(t)
    assert items == [task_start]


def test_tasks_captures_start_and_result() -> None:
    t = TasksTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    start = {"id": "t1", "name": "a", "input": None, "triggers": []}
    result = {"id": "t1", "name": "a", "result": {"output": 42}, "error": None}
    mux.push(_tasks_event([], start))
    mux.push(_tasks_event([], result))

    items = _drain(t)
    assert items == [start, result]


def test_tasks_ignores_subgraph_events() -> None:
    t = TasksTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_tasks_event(["child:abc"], {"id": "t1", "name": "x"}))

    assert _drain(t) == []


def test_tasks_scoped_transformer() -> None:
    t = TasksTransformer(scope=("agent:abc",))
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_tasks_event([], {"id": "t1"}))
    mux.push(_tasks_event(["agent:abc"], {"id": "t2"}))
    mux.push(_tasks_event(["agent:abc", "deep:def"], {"id": "t3"}))

    assert _drain(t) == [{"id": "t2"}]


def test_tasks_does_not_suppress_from_main_log() -> None:
    """TasksTransformer returns True — it doesn't suppress tasks events.

    (LifecycleTransformer suppresses them, but that's independent.)
    """
    t = TasksTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_tasks_event([], {"id": "t1"}))

    methods = [evt["method"] for evt in _unstamped(mux._events._items)]
    assert "tasks" in methods


def test_tasks_required_stream_modes() -> None:
    assert TasksTransformer.required_stream_modes == ("tasks",)


def test_tasks_is_native() -> None:
    assert getattr(TasksTransformer, "_native", False) is True


# ---------------------------------------------------------------------------
# UpdatesTransformer
# ---------------------------------------------------------------------------


def test_updates_captures_root_scope_events() -> None:
    t = UpdatesTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    update = {"my_node": {"value": "hello!"}}
    mux.push(_updates_event([], update))

    items = _drain(t)
    assert items == [update]


def test_updates_captures_multiple_steps() -> None:
    t = UpdatesTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_updates_event([], {"node_a": {"x": 1}}))
    mux.push(_updates_event([], {"node_b": {"x": 2}}))

    items = _drain(t)
    assert items == [{"node_a": {"x": 1}}, {"node_b": {"x": 2}}]


def test_updates_ignores_subgraph_events() -> None:
    t = UpdatesTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_updates_event(["child:abc"], {"inner_node": {"v": 1}}))

    assert _drain(t) == []


def test_updates_scoped_transformer() -> None:
    t = UpdatesTransformer(scope=("agent:abc",))
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_updates_event([], {"from": "root"}))
    mux.push(_updates_event(["agent:abc"], {"from": "self"}))

    assert _drain(t) == [{"from": "self"}]


def test_updates_does_not_suppress_from_main_log() -> None:
    t = UpdatesTransformer()
    mux = StreamMux([t], is_async=False)
    _arm(mux, t)

    mux.push(_updates_event([], {"n": {}}))

    methods = [evt["method"] for evt in _unstamped(mux._events._items)]
    assert "updates" in methods


def test_updates_required_stream_modes() -> None:
    assert UpdatesTransformer.required_stream_modes == ("updates",)


def test_updates_is_native() -> None:
    assert getattr(UpdatesTransformer, "_native", False) is True


# ---------------------------------------------------------------------------
# Cross-transformer: unrelated events pass through
# ---------------------------------------------------------------------------


def test_unrelated_events_ignored_by_all() -> None:
    """Non-matching method events don't land in any transformer's log."""
    transformers = [
        CustomTransformer(),
        UpdatesTransformer(),
        CheckpointsTransformer(),
        DebugTransformer(),
        TasksTransformer(),
    ]
    mux = StreamMux(transformers, is_async=False)
    mux._events._subscribed = True
    for t in transformers:
        t._log._subscribed = True

    mux.push(
        {
            "type": "event",
            "method": "values",
            "params": {"namespace": [], "timestamp": TS, "data": {"x": 1}},
        }
    )

    for t in transformers:
        assert _unstamped(t._log._items) == []


# ---------------------------------------------------------------------------
# End-to-end: real graphs through stream_events(version="v3")
# ---------------------------------------------------------------------------


class _State(TypedDict):
    value: str
    items: Annotated[list[str], operator.add]


def _my_node(state: _State) -> dict[str, Any]:
    from langgraph.config import get_stream_writer

    writer = get_stream_writer()
    writer({"status": "working", "node": "my_node"})
    return {"value": state["value"] + "!", "items": ["done"]}


def _make_simple_graph() -> Any:
    builder = StateGraph(_State, input_schema=_State)
    builder.add_node("my_node", _my_node)
    builder.add_edge(START, "my_node")
    builder.add_edge("my_node", END)
    return builder.compile()


def test_stream_events_v3_custom_projection_opt_in() -> None:
    """run.custom surfaces get_stream_writer() payloads when opted in."""
    graph = _make_simple_graph()
    run = graph.stream_events(
        {"value": "hello", "items": []}, version="v3", transformers=[CustomTransformer]
    )

    custom_events = list(run.custom)
    assert len(custom_events) >= 1
    assert any(e.get("status") == "working" for e in custom_events)


def test_stream_events_v3_custom_and_values_coexist() -> None:
    """Both run.custom and run.values work in the same run."""
    graph = _make_simple_graph()
    run = graph.stream_events(
        {"value": "hello", "items": []}, version="v3", transformers=[CustomTransformer]
    )

    custom_events = list(run.custom)
    assert run.output is not None
    assert run.output["value"] == "hello!"
    assert len(custom_events) >= 1


def test_stream_events_v3_tasks_projection_opt_in() -> None:
    """run.tasks surfaces raw task events when opted in via transformers=."""
    graph = _make_simple_graph()
    run = graph.stream_events(
        {"value": "x", "items": []}, transformers=[TasksTransformer], version="v3"
    )

    tasks_events = list(run.tasks)
    assert len(tasks_events) >= 1
    names = [t.get("name") for t in tasks_events if "name" in t]
    assert "my_node" in names


def test_stream_events_v3_debug_projection_opt_in() -> None:
    """run.debug surfaces debug events when opted in via transformers=."""
    graph = _make_simple_graph()
    run = graph.stream_events(
        {"value": "x", "items": []}, transformers=[DebugTransformer], version="v3"
    )

    debug_events = list(run.debug)
    assert len(debug_events) >= 1
    types = {d.get("type") for d in debug_events}
    assert types & {"checkpoint", "task", "task_result"}


def test_stream_events_v3_updates_projection_opt_in() -> None:
    """run.updates surfaces node output dicts when opted in via transformers=."""
    graph = _make_simple_graph()
    run = graph.stream_events(
        {"value": "x", "items": []}, version="v3", transformers=[UpdatesTransformer]
    )

    updates = list(run.updates)
    assert len(updates) >= 1
    node_names = {k for u in updates for k in u if k != "__interrupt__"}
    assert "my_node" in node_names


def test_stream_events_v3_all_transformers_interleaved() -> None:
    """All five transformers registered together, consumed via interleave."""
    graph = _make_simple_graph()
    run = graph.stream_events(
        {"value": "x", "items": []},
        version="v3",
        transformers=[
            CustomTransformer,
            UpdatesTransformer,
            CheckpointsTransformer,
            DebugTransformer,
            TasksTransformer,
        ],
    )

    collected: dict[str, list[Any]] = {
        "custom": [],
        "updates": [],
        "debug": [],
        "tasks": [],
    }
    for name, item in run.interleave("custom", "updates", "debug", "tasks"):
        collected[name].append(item)

    assert len(collected["custom"]) >= 1
    assert len(collected["updates"]) >= 1
    assert len(collected["tasks"]) >= 1
    assert len(collected["debug"]) >= 1
    types = {d.get("type") for d in collected["debug"]}
    assert types & {"checkpoint", "task", "task_result"}
    node_names = {k for u in collected["updates"] for k in u if k != "__interrupt__"}
    assert "my_node" in node_names

    assert run.output is not None
    assert run.output["value"] == "x!"


def test_stream_events_v3_all_transformers_with_checkpointer() -> None:
    """All transformers with a checkpointer — run.checkpoints populated."""
    from langgraph.checkpoint.memory import InMemorySaver

    builder = StateGraph(_State, input_schema=_State)
    builder.add_node("my_node", _my_node)
    builder.add_edge(START, "my_node")
    builder.add_edge("my_node", END)
    graph = builder.compile(checkpointer=InMemorySaver())

    run = graph.stream_events(
        {"value": "x", "items": []},
        version="v3",
        config={"configurable": {"thread_id": "test-all"}},
        transformers=[
            CustomTransformer,
            UpdatesTransformer,
            CheckpointsTransformer,
            DebugTransformer,
            TasksTransformer,
        ],
    )

    collected: dict[str, list[Any]] = {
        "custom": [],
        "updates": [],
        "checkpoints": [],
        "debug": [],
        "tasks": [],
    }
    for name, item in run.interleave(
        "custom", "updates", "checkpoints", "debug", "tasks"
    ):
        collected[name].append(item)

    assert len(collected["checkpoints"]) >= 1
    assert len(collected["custom"]) >= 1


def test_stream_events_v3_checkpoints_projection_opt_in() -> None:
    """run.checkpoints surfaces checkpoint data when opted in with a checkpointer."""
    from langgraph.checkpoint.memory import InMemorySaver

    builder = StateGraph(_State, input_schema=_State)
    builder.add_node("my_node", _my_node)
    builder.add_edge(START, "my_node")
    builder.add_edge("my_node", END)
    graph = builder.compile(checkpointer=InMemorySaver())

    run = graph.stream_events(
        {"value": "x", "items": []},
        version="v3",
        config={"configurable": {"thread_id": "test-ckpt-standalone"}},
        transformers=[CheckpointsTransformer],
    )

    checkpoints = list(run.checkpoints)
    assert len(checkpoints) >= 1


# ---------------------------------------------------------------------------
# TasksTransformer + LifecycleTransformer co-registration
# ---------------------------------------------------------------------------


def test_tasks_and_lifecycle_coregistration() -> None:
    """When both are in the same StreamMux, LifecycleTransformer suppresses
    tasks events from the main log (returns False) while TasksTransformer
    still captures them into its own log.
    """
    lifecycle = LifecycleTransformer()
    tasks = TasksTransformer()
    mux = StreamMux([lifecycle, tasks], is_async=False)
    mux._events._subscribed = True
    tasks._log._subscribed = True
    lifecycle._channel._subscribed = True

    task_data = {"id": "t1", "name": "my_node", "input": None, "triggers": []}
    mux.push(_tasks_event([], task_data))

    assert _drain(tasks) == [task_data]

    methods = [evt["method"] for evt in _unstamped(mux._events._items)]
    assert "tasks" not in methods


def test_tasks_and_lifecycle_coregistration_e2e() -> None:
    """E2e: TasksTransformer captures task events even when LifecycleTransformer
    is present and suppressing them from the main log.
    """
    graph = _make_simple_graph()
    run = graph.stream_events(
        {"value": "x", "items": []},
        version="v3",
        transformers=[TasksTransformer],
    )

    tasks_events = list(run.tasks)
    assert len(tasks_events) >= 1
    names = [t.get("name") for t in tasks_events if "name" in t]
    assert "my_node" in names
