import json

from backend.services.agent_service import AgentService
from backend.services.session_store import AgentSession, SessionStore


class _DummyAgent:
    def invoke(self, _payload):
        return {"output": "ok"}


def test_session_file_context_does_not_leak_between_sessions(monkeypatch):
    store = SessionStore()
    store.add_file("session-a", "/tmp/a.mseed")
    store.set_current_file("session-a", "/tmp/a.mseed")
    store.add_file("session-b", "/tmp/b.segy")
    store.set_current_file("session-b", "/tmp/b.segy")

    service = AgentService(session_store=store)

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_DummyAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)

    injected: list[tuple[str, str]] = []

    def _fake_bind(path: str, file_type: str) -> bool:
        injected.append((path, file_type))
        return True

    monkeypatch.setattr("backend.services.agent_service.bind_uploaded_file_to_agent", _fake_bind)

    result_a = service.chat("hello", session_id="session-a", lang="en")
    result_b = service.chat("hello", session_id="session-b", lang="en")

    assert result_a.error is None
    assert result_b.error is None
    assert injected == [
        ("/tmp/a.mseed", "miniseed"),
        ("/tmp/b.segy", "segy"),
    ]


def test_agent_service_returns_structured_fallback_on_react_parse_error(monkeypatch):
    store = SessionStore()
    service = AgentService(session_store=store)

    class _BrokenAgent:
        def invoke(self, _payload):
            raise RuntimeError("Invalid Format: Missing 'Action:' after 'Thought:'")

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_BrokenAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    monkeypatch.setattr(
        service._tool_planner,
        "plan",
        lambda **_: type("P", (), {"route": "phase_picking", "tool": "", "params": {}, "need_rerun": False, "confidence": 0.0})(),
    )

    result = service.chat("对当前波形做初至拾取", session_id="sid-react", lang="zh")

    assert result.route == "phase_picking"
    assert result.answer == "工具执行已完成，但 Agent 输出格式异常。请查看已生成结果或重试。"
    assert "Invalid Format" in (result.error or "")


def test_session_store_runtime_results_roundtrip():
    store = SessionStore()
    store.set_runtime_result("sid", "last_catalog_csv", "location/a.csv")
    store.update_runtime_results("sid", {"last_catalog_json": "location/a.json"})
    store.update_runtime_results("sid", {"files": {"A.mseed": {"picks_csv": "picks/a.csv"}}})
    store.update_runtime_results("sid", {"files": {"B.mseed": {"picks_csv": "picks/b.csv"}}})

    payload = store.get_runtime_results("sid")
    assert payload["last_catalog_csv"] == "location/a.csv"
    assert payload["last_catalog_json"] == "location/a.json"
    assert payload["files"]["A.mseed"]["picks_csv"] == "picks/a.csv"
    assert payload["files"]["B.mseed"]["picks_csv"] == "picks/b.csv"


def test_agent_service_injects_runtime_context_and_updates_runtime(monkeypatch):
    store = SessionStore()
    store.update_runtime_results("sid", {"last_catalog_csv": "location/old.csv"})
    captured: dict[str, str] = {}

    class _CaptureAgent:
        def invoke(self, payload):
            captured["input"] = str(payload.get("input", ""))
            return {
                "output": json.dumps(
                    {
                        "success": True,
                        "message": "ok",
                        "data": {"catalog_csv": "data/location/new.csv"},
                        "artifacts": [
                            {
                                "type": "file",
                                "name": "new.csv",
                                "path": "location/new.csv",
                                "url": "/api/artifacts/location/new.csv",
                            }
                        ],
                    },
                    ensure_ascii=False,
                )
            }

    service = AgentService(session_store=store)

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_CaptureAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    result = service.chat("hello", session_id="sid", lang="zh")

    assert result.error is None
    assert "当前会话已有结果上下文" in captured["input"]
    runtime = store.get_runtime_results("sid")
    assert runtime["last_catalog_csv"] == "location/new.csv"


def test_agent_service_trace_pick_fast_path_skips_react(monkeypatch):
    store = SessionStore()
    store.add_file("sid-fast", "/tmp/demo.mseed")
    store.set_current_file("sid-fast", "/tmp/demo.mseed")
    service = AgentService(session_store=store)

    class _ShouldNotRunAgent:
        def invoke(self, _payload):
            raise AssertionError("ReAct agent should not be invoked for trace-pick fast path")

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_ShouldNotRunAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    monkeypatch.setattr(service, "_summarize_direct_tool_result", lambda **_: "summary")
    monkeypatch.setattr(
        service._tool_planner,
        "plan",
        lambda **_: type(
            "P",
            (),
            {
                "route": "result_analysis",
                "tool": "pick_first_arrivals",
                "params": {"trace_number": 1},
                "need_rerun": False,
                "confidence": 1.0,
            },
        )(),
    )
    monkeypatch.setattr(
        "backend.services.agent_service.pick_first_arrivals",
        type(
            "_DummyTool",
            (),
            {
                "invoke": staticmethod(
                    lambda payload: {
                        "success": True,
                        "message": "初至拾取已完成。\\n图表已保存至：`data/picks/demo.png`",
                        "artifacts": [
                            {
                                "type": "image",
                                "name": "demo.png",
                                "path": "picks/demo.png",
                                "url": "/api/artifacts/picks/demo.png",
                            }
                        ],
                        "data": {"plot_path": "picks/demo.png"},
                    }
                )
            },
        ),
    )

    result = service.chat("查看 trace 1 的拾取结果", session_id="sid-fast", lang="zh")
    assert result.error is None
    assert result.route == "result_analysis"
    assert result.answer == "summary"
    assert result.artifacts and result.artifacts[0].url == "/api/artifacts/picks/demo.png"
    runtime = store.get_runtime_results("sid-fast")
    assert runtime.get("last_miniseed_file") == "tmp/demo.mseed"
    assert runtime.get("last_uploaded_files") == ["tmp/demo.mseed"]
    assert runtime.get("last_analysis_artifacts")


def test_agent_service_bootstraps_pick_for_picks_analysis_without_runtime_csv(monkeypatch):
    store = SessionStore()
    store.add_file("sid-bootstrap", "/tmp/demo.mseed")
    store.set_current_file("sid-bootstrap", "/tmp/demo.mseed")
    service = AgentService(session_store=store)

    class _ShouldNotRunAgent:
        def invoke(self, _payload):
            raise AssertionError("ReAct agent should not be invoked for bootstrap picking")

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_ShouldNotRunAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    monkeypatch.setattr(service, "_summarize_direct_tool_result", lambda **_: "summary-bootstrap")
    monkeypatch.setattr(
        service._tool_planner,
        "plan",
        lambda **_: type(
            "P",
            (),
            {
                "route": "result_analysis",
                "tool": "picks_trace_plot",
                "params": {"trace_index": 0},
                "need_rerun": False,
                "confidence": 0.9,
            },
        )(),
    )
    monkeypatch.setattr(
        "backend.services.agent_service.pick_first_arrivals",
        type(
            "_DummyTool",
            (),
            {
                "invoke": staticmethod(
                    lambda payload: {
                        "success": True,
                        "message": "初至拾取已完成。",
                        "artifacts": [
                            {
                                "type": "image",
                                "name": "demo.png",
                                "path": "picks/demo.png",
                                "url": "/api/artifacts/picks/demo.png",
                            },
                            {
                                "type": "file",
                                "name": "demo.csv",
                                "path": "picks/demo.csv",
                                "url": "/api/artifacts/picks/demo.csv",
                            },
                        ],
                        "data": {"picks_csv": "picks/demo.csv", "plot_path": "picks/demo.png"},
                    }
                )
            },
        ),
    )

    result = service.chat("看看第一道的拾取图像", session_id="sid-bootstrap", lang="zh")
    assert result.error is None
    assert result.route in {"phase_picking", "result_analysis"}
    assert result.answer
    runtime = store.get_runtime_results("sid-bootstrap")
    assert runtime.get("last_picks_csv") == "picks/demo.csv"
    assert runtime.get("last_picks_image") == "picks/demo.png"
    assert runtime.get("last_picks_source_file") == "tmp/demo.mseed"
    assert runtime.get("active_file") == "demo.mseed"
    assert runtime.get("files", {}).get("demo.mseed", {}).get("picks_csv") == "picks/demo.csv"


def test_agent_service_bootstraps_pick_when_picks_source_mismatch(monkeypatch):
    store = SessionStore()
    store.add_file("sid-mismatch", "/tmp/new.mseed")
    store.set_current_file("sid-mismatch", "/tmp/new.mseed")
    store.update_runtime_results(
        "sid-mismatch",
        {
            "last_picks_csv": "picks/old.csv",
            "last_picks_source_file": "tmp/old.mseed",
        },
    )
    service = AgentService(session_store=store)

    class _ShouldNotRunAgent:
        def invoke(self, _payload):
            raise AssertionError("ReAct agent should not be invoked for mismatch bootstrap")

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_ShouldNotRunAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    monkeypatch.setattr(service, "_summarize_direct_tool_result", lambda **_: "summary-mismatch")
    monkeypatch.setattr(
        service._tool_planner,
        "plan",
        lambda **_: type(
            "P",
            (),
            {
                "route": "result_analysis",
                "tool": "picks_trace_plot",
                "params": {"trace_index": 1},
                "need_rerun": False,
                "confidence": 0.9,
            },
        )(),
    )
    monkeypatch.setattr(
        "backend.services.agent_service.pick_first_arrivals",
        type(
            "_DummyTool",
            (),
            {
                "invoke": staticmethod(
                    lambda payload: {
                        "success": True,
                        "message": "初至拾取已完成。",
                        "artifacts": [
                            {
                                "type": "file",
                                "name": "new.csv",
                                "path": "picks/new.csv",
                                "url": "/api/artifacts/picks/new.csv",
                            }
                        ],
                        "data": {"picks_csv": "picks/new.csv"},
                    }
                )
            },
        ),
    )

    result = service.chat("看看第二道拾取的结果图", session_id="sid-mismatch", lang="zh")
    assert result.error is None
    assert result.answer == "summary-mismatch"
    runtime = store.get_runtime_results("sid-mismatch")
    assert runtime.get("last_picks_csv") == "picks/new.csv"
    assert runtime.get("last_picks_source_file") == "tmp/new.mseed"


def test_agent_service_continuous_monitoring_uses_planned_params(monkeypatch):
    store = SessionStore()
    service = AgentService(session_store=store)

    class _ShouldNotRunAgent:
        def invoke(self, _payload):
            raise AssertionError("ReAct agent should not be invoked for continuous monitoring")

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_ShouldNotRunAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    monkeypatch.setattr(service._router_service, "route_intent", lambda _message: "continuous_monitoring")
    monkeypatch.setattr(service, "_summarize_direct_tool_result", lambda **_: "continuous-summary")

    captured: dict[str, object] = {}

    monkeypatch.setattr(
        service._tool_planner,
        "plan",
        lambda **_: type(
            "P",
            (),
            {
                "route": "continuous_monitoring",
                "tool": "run_continuous_monitoring",
                "params": {
                    "region": "加州",
                    "start": "2019-07-04T17:00:00",
                    "end": "2019-07-04T18:00:00",
                },
                "need_rerun": True,
                "confidence": 0.95,
            },
        )(),
    )
    monkeypatch.setattr(
        "backend.services.agent_service.run_continuous_monitoring",
        type(
            "_DummyTool",
            (),
            {
                "invoke": staticmethod(
                    lambda payload: (
                        captured.setdefault("payload", payload),
                        {
                            "success": True,
                            "message": "ok",
                            "data": {"start_time": "2019-07-04T17:00:00", "end_time": "2019-07-04T18:00:00"},
                        },
                    )[1]
                )
            },
        ),
    )

    result = service.chat("对加州2019年7月4日的17到18点进行地震监测", session_id="sid-cm", lang="zh")

    assert result.error is None
    assert result.answer == "continuous-summary"
    assert captured["payload"] == {
        "params": {
            "region": "加州",
            "start": "2019-07-04T17:00:00",
            "end": "2019-07-04T18:00:00",
        }
    }


def test_agent_service_apply_file_reference_from_message_activates_file_record():
    store = SessionStore()
    store.add_file("sid-ref", "/tmp/CI.IDO.mseed")
    store.add_file("sid-ref", "/tmp/CI.SMR.mseed")
    store.set_current_file("sid-ref", "/tmp/CI.SMR.mseed")
    store.update_runtime_results(
        "sid-ref",
        {
            "files": {
                "CI.IDO.mseed": {
                    "source_file": "tmp/CI.IDO.mseed",
                    "picks_csv": "picks/ido.csv",
                    "picks_image": "picks/ido.png",
                }
            }
        },
    )
    service = AgentService(session_store=store)

    service._apply_file_reference_from_message("sid-ref", "分析一下这个文件的拾取情况 CI.IDO.mseed")

    runtime = store.get_runtime_results("sid-ref")
    assert runtime.get("active_file") == "CI.IDO.mseed"
    assert runtime.get("last_picks_csv") == "picks/ido.csv"
    assert runtime.get("last_picks_image") == "picks/ido.png"
    assert runtime.get("last_miniseed_file") == "tmp/CI.IDO.mseed"
    assert store.get_current_file("sid-ref") == "tmp/CI.IDO.mseed"


def test_agent_service_file_structure_fast_path(monkeypatch):
    store = SessionStore()
    service = AgentService(session_store=store)

    class _ShouldNotRunAgent:
        def invoke(self, _payload):
            raise AssertionError("ReAct agent should not be invoked for file_structure fast path")

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_ShouldNotRunAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    monkeypatch.setattr(service, "_summarize_direct_tool_result", lambda **_: "summary-struct")
    monkeypatch.setattr(
        "backend.services.agent_service.get_file_structure",
        type("_DummyTool", (), {"invoke": staticmethod(lambda _payload: "{\"type\":\"miniseed\"}")}),
    )

    result = service.chat("读取当前文件结构", session_id="sid-struct", lang="zh")
    assert result.error is None
    assert result.route == "file_structure"
    assert result.answer == "summary-struct"


def test_agent_service_format_conversion_fast_path(monkeypatch):
    store = SessionStore()
    store.add_file("sid-conv", "/tmp/demo.mseed")
    store.set_current_file("sid-conv", "/tmp/demo.mseed")
    service = AgentService(session_store=store)

    class _ShouldNotRunAgent:
        def invoke(self, _payload):
            raise AssertionError("ReAct agent should not be invoked for conversion fast path")

    def _fake_build_agent_session(session_id: str, lang: str) -> AgentSession:
        return AgentSession(session_id=session_id, lang=lang, agent=_ShouldNotRunAgent())

    monkeypatch.setattr(service, "_build_agent_session", _fake_build_agent_session)
    monkeypatch.setattr(service, "_summarize_direct_tool_result", lambda **_: "summary-convert")
    monkeypatch.setattr(
        "backend.services.agent_service.convert_miniseed_to_hdf5",
        type(
            "_DummyTool",
            (),
            {
                "invoke": staticmethod(
                    lambda _payload: {
                        "success": True,
                        "message": "converted",
                        "artifacts": [
                            {
                                "type": "file",
                                "name": "demo.h5",
                                "path": "convert/demo.h5",
                                "url": "/api/artifacts/convert/demo.h5",
                            }
                        ],
                    }
                )
            },
        ),
    )

    result = service.chat("把这个文件转换成 hdf5", session_id="sid-conv", lang="zh")
    assert result.error is None
    assert result.route == "format_conversion"
    assert result.answer == "summary-convert"
    assert result.artifacts and result.artifacts[0].url == "/api/artifacts/convert/demo.h5"
    runtime = store.get_runtime_results("sid-conv")
    assert runtime.get("last_converted_file") == "convert/demo.h5"
    assert runtime.get("last_uploaded_files") == ["tmp/demo.mseed"]
