from __future__ import annotations

import io
import json
import tempfile
import threading
import unittest
from dataclasses import dataclass
from pathlib import Path
from unittest.mock import patch

from memnixa.config import AppConfig
from memnixa.gateway.cli import (
    _run_one_turn,
    build_parser,
    run_identity_bind_owner_command,
    run_config_add_model_command,
    run_config_open_command,
    run_config_path_command,
    run_config_sync_command,
    run_data_dump_command,
)
from memnixa.gateway.messages import InboundMessageContext
from memnixa.gateway.server import GatewayApplication
from memnixa.models import RunTurnResult


@dataclass
class FakeRuntime:
    config: AppConfig
    last_inbound: InboundMessageContext | None = None

    def run_turn(
        self,
        inbound: InboundMessageContext,
        *,
        event_callback=None,
    ) -> RunTurnResult:
        self.last_inbound = inbound
        return RunTurnResult(
            reply=f"echo:{inbound.body_for_agent}",
            iterations=1,
            tool_events=[{"tool_name": "fake_tool", "arguments": {}, "result": "ok"}],
            provider_id="1",
            provider_label="Model One",
            model_name="model-one",
        )


class GatewayTests(unittest.TestCase):
    def test_gateway_application_executes_runtime(self) -> None:
        config = AppConfig(api_key="test-key")
        runtime = FakeRuntime(config=config)
        application = GatewayApplication(runtime=runtime)
        result = application.run_agent(
            {
                "session_id": "main",
                "run_id": "run-1",
                "message_id": "msg-1",
                "raw_body": "hello gateway",
                "body_for_agent": "hello gateway",
                "received_at": "2026-03-13T00:00:00+00:00",
                "source_mode": "cli_gateway",
                "channel": "cli",
                "launch_cwd": "/tmp/demo-launch",
                "entrypoint_path": "/usr/local/bin/memnixa",
                "python_executable": "/usr/bin/python3.12",
            }
        )

        self.assertTrue(result["ok"])
        self.assertEqual(result["reply"], "echo:hello gateway")
        self.assertEqual(result["provider_id"], "1")
        self.assertEqual(result["tool_events"][0]["tool_name"], "fake_tool")
        self.assertIsNotNone(runtime.last_inbound)
        self.assertEqual(runtime.last_inbound.source_mode, "cli_gateway")
        self.assertEqual(runtime.last_inbound.channel, "cli")
        self.assertEqual(runtime.last_inbound.launch_cwd, "/tmp/demo-launch")
        self.assertEqual(runtime.last_inbound.entrypoint_path, "/usr/local/bin/memnixa")
        self.assertEqual(runtime.last_inbound.python_executable, "/usr/bin/python3.12")
        self.assertEqual(runtime.last_inbound.actor_identity_status, "owner")
        self.assertEqual(runtime.last_inbound.actor_user_id, "owner")
        self.assertTrue(runtime.last_inbound.actor_is_owner)
        self.assertIn("queue", application.health())
        application.shutdown()

    def test_gateway_application_serializes_same_session_requests(self) -> None:
        @dataclass
        class BlockingRuntime:
            config: AppConfig

            def __post_init__(self) -> None:
                self.active = 0
                self.max_active = 0
                self.lock = threading.Lock()
                self.first_started = threading.Event()
                self.second_started = threading.Event()
                self.release_first = threading.Event()
                self.order: list[str] = []

            def run_turn(
                self,
                inbound: InboundMessageContext,
                *,
                event_callback=None,
            ) -> RunTurnResult:
                with self.lock:
                    self.active += 1
                    self.max_active = max(self.max_active, self.active)
                    self.order.append(f"start:{inbound.message_id}")
                if inbound.message_id == "msg-1":
                    self.first_started.set()
                    self.release_first.wait(timeout=2)
                else:
                    self.second_started.set()
                with self.lock:
                    self.active -= 1
                    self.order.append(f"end:{inbound.message_id}")
                return RunTurnResult(
                    reply=f"echo:{inbound.body_for_agent}",
                    iterations=1,
                    provider_id="1",
                    provider_label="Model One",
                    model_name="model-one",
                )

        runtime = BlockingRuntime(config=AppConfig(api_key="test-key"))
        application = GatewayApplication(runtime=runtime)
        payload_one = {
            "session_id": "main",
            "message_id": "msg-1",
            "body_for_agent": "hello",
        }
        payload_two = {
            "session_id": "main",
            "message_id": "msg-2",
            "body_for_agent": "again",
        }
        results: list[dict] = []

        thread_one = threading.Thread(
            target=lambda: results.append(application.run_agent(payload_one))
        )
        thread_two = threading.Thread(
            target=lambda: results.append(application.run_agent(payload_two))
        )
        thread_one.start()
        self.assertTrue(runtime.first_started.wait(timeout=1))
        thread_two.start()
        self.assertFalse(runtime.second_started.wait(timeout=0.1))
        runtime.release_first.set()
        thread_one.join(timeout=2)
        thread_two.join(timeout=2)

        self.assertEqual(len(results), 2)
        self.assertEqual(runtime.max_active, 1)
        self.assertEqual(
            runtime.order,
            ["start:msg-1", "end:msg-1", "start:msg-2", "end:msg-2"],
        )
        application.shutdown()

    def test_gateway_mode_calls_gateway_client(self) -> None:
        with patch("memnixa.gateway.cli.GatewayClient.run_agent") as run_agent:
            run_agent.return_value = {
                "ok": True,
                "session_id": "main",
                "run_id": "run-1",
                "message_id": "msg-1",
                "reply": "echo:hello gateway",
                "iterations": 1,
                "tool_events": [],
                "provider_id": "1",
                "provider_label": "Model One",
                "model_name": "model-one",
            }
            result = _run_one_turn(
                message="hello gateway",
                via="gateway",
                config=None,
                gateway_url="http://127.0.0.1:8787",
            )

        self.assertTrue(result["ok"])
        self.assertEqual(result["mode"], "gateway")
        self.assertEqual(result["provider_id"], "1")
        run_agent.assert_called_once()
        inbound = run_agent.call_args.args[0]
        self.assertEqual(inbound.channel, "cli")
        self.assertEqual(inbound.source_mode, "cli_gateway")
        self.assertIsNotNone(inbound.launch_cwd)
        self.assertIsNotNone(inbound.entrypoint_path)
        self.assertIsNotNone(inbound.python_executable)
        self.assertEqual(inbound.actor_identity_status, "owner")
        self.assertEqual(inbound.actor_user_id, "owner")
        self.assertTrue(inbound.actor_is_owner)

    def test_auto_mode_falls_back_to_local(self) -> None:
        config = AppConfig(api_key="test-key")
        fake_app = type(
            "FakeApp",
            (),
            {"runtime": FakeRuntime(config=config)},
        )()
        with patch(
            "memnixa.gateway.cli._run_via_gateway",
            side_effect=RuntimeError("gateway down"),
        ):
            with patch("memnixa.gateway.cli.build_app", return_value=fake_app):
                result = _run_one_turn(
                    message="hello local",
                    via="auto",
                    config=config,
                    gateway_url="http://127.0.0.1:9999",
                )

        self.assertTrue(result["ok"])
        self.assertEqual(result["mode"], "local")
        self.assertEqual(result["provider_id"], "1")
        self.assertEqual(result["meta"]["fallback"], "local")

    def test_local_mode_forwards_runtime_event_callback(self) -> None:
        config = AppConfig(api_key="test-key")

        @dataclass
        class StreamingRuntime:
            config: AppConfig

            def run_turn(
                self,
                inbound: InboundMessageContext,
                *,
                event_callback=None,
            ) -> RunTurnResult:
                event = {
                    "type": "memory",
                    "phase": "error",
                    "message": "Network timeout after 9s",
                }
                if event_callback is not None:
                    event_callback(event)
                return RunTurnResult(
                    reply=f"echo:{inbound.body_for_agent}",
                    iterations=1,
                    runtime_events=[event],
                    provider_id="1",
                    provider_label="Model One",
                    model_name="model-one",
                )

        fake_app = type(
            "FakeApp",
            (),
            {"runtime": StreamingRuntime(config=config)},
        )()
        seen_events: list[dict] = []

        with patch("memnixa.gateway.cli.build_app", return_value=fake_app):
            result = _run_one_turn(
                message="hello local",
                via="local",
                config=config,
                gateway_url=None,
                event_callback=seen_events.append,
            )

        self.assertEqual(seen_events, result["runtime_events"])
        self.assertEqual(result["runtime_events"][0]["message"], "Network timeout after 9s")

    def test_cli_via_can_come_from_config(self) -> None:
        config = AppConfig(api_key="test-key", cli_via="gateway")
        with patch("memnixa.gateway.cli._run_via_gateway") as run_via_gateway:
            run_via_gateway.return_value = {
                "ok": True,
                "session_id": "main",
                "run_id": "run-1",
                "message_id": "msg-1",
                "reply": "echo:config gateway",
                "iterations": 1,
                "tool_events": [],
                "provider_id": "1",
                "provider_label": "Model One",
                "model_name": "model-one",
            }
            with patch("memnixa.gateway.cli.load_config", return_value=config):
                from memnixa.gateway.cli import run_agent_command

                run_agent_command(
                    config_path=None,
                    first_message="hello",
                    via=None,
                    gateway_url="http://127.0.0.1:8787",
                    gateway_host=None,
                    gateway_port=None,
                )

        run_via_gateway.assert_called_once()

    def test_config_commands_are_exposed_in_parser(self) -> None:
        parser = build_parser()

        sync_args = parser.parse_args(["config", "sync"])
        path_args = parser.parse_args(["config", "path"])
        open_args = parser.parse_args(["config", "open"])
        add_model_args = parser.parse_args(
            [
                "config",
                "add-model",
                "--provider",
                "siliconflow",
                "--id",
                "3",
                "--api-key",
                "key",
                "--model",
                "deepseek-ai/DeepSeek-V3",
            ]
        )
        dump_args = parser.parse_args(["data", "dump"])
        identity_args = parser.parse_args(
            ["identity", "bind-owner", "--channel", "qq", "--external-id", "user-1"]
        )

        self.assertEqual(sync_args.config_command, "sync")
        self.assertEqual(path_args.config_command, "path")
        self.assertEqual(open_args.config_command, "open")
        self.assertEqual(add_model_args.config_command, "add-model")
        self.assertEqual(dump_args.data_command, "dump")
        self.assertEqual(identity_args.identity_command, "bind-owner")

    def test_config_sync_command_prints_added_fields(self) -> None:
        buffer = io.StringIO()
        with patch(
            "memnixa.gateway.cli.sync_default_config",
            return_value=(
                Path("/tmp/.memnixa/config.json"),
                False,
                ["default_provider"],
            ),
        ):
            with patch("sys.stdout", buffer):
                run_config_sync_command(show_path=False)

        output = buffer.getvalue()
        self.assertIn("Memnixa config updated", output)
        self.assertIn("default_provider", output)

    def test_parser_description_switches_with_locale(self) -> None:
        with patch.dict("os.environ", {"MEMNIXA_LANG": "zh-CN"}, clear=False):
            parser = build_parser()

        self.assertIn("命令行", parser.description)

    def test_config_path_command_prints_default_path(self) -> None:
        buffer = io.StringIO()
        with patch("sys.stdout", buffer):
            run_config_path_command()

        self.assertIn(".memnixa/config.json", buffer.getvalue())

    def test_config_add_model_command_auto_generates_id_for_builtin_provider(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            config_path = Path(temp_dir) / "config.json"
            config_path.write_text(
                json.dumps(
                    {
                        "default_provider": "1",
                        "providers": [
                            {
                                "id": "1",
                                "preset": "openai_chatgpt",
                                "api_key": "openai-key",
                                "model": "gpt-4.1-mini",
                            }
                        ],
                    }
                ),
                encoding="utf-8",
            )
            buffer = io.StringIO()

            with patch("sys.stdout", buffer):
                run_config_add_model_command(
                    config_path=config_path,
                    provider="siliconflow",
                    provider_id=None,
                    label=None,
                    api_key="sf-key",
                    model="deepseek-ai/DeepSeek-V3",
                    api_base=None,
                    set_default=True,
                )

            updated = json.loads(config_path.read_text(encoding="utf-8"))

        self.assertEqual(updated["default_provider"], "deepseek-ai/DeepSeek-V3")
        self.assertEqual(updated["providers"][-1]["id"], "deepseek-ai/DeepSeek-V3")
        self.assertEqual(updated["providers"][-1]["preset"], "siliconflow")
        self.assertEqual(
            updated["providers"][-1]["api_base"],
            "https://api.siliconflow.cn/v1",
        )
        self.assertEqual(updated["providers"][-1]["label"], "SiliconFlow")
        self.assertIn("deepseek-ai/DeepSeek-V3", buffer.getvalue())

    def test_config_add_model_command_deduplicates_auto_generated_id(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            config_path = Path(temp_dir) / "config.json"
            config_path.write_text(
                json.dumps(
                    {
                        "default_provider": "deepseek-ai/DeepSeek-V3",
                        "providers": [
                            {
                                "id": "deepseek-ai/DeepSeek-V3",
                                "preset": "siliconflow",
                                "api_key": "sf-key-1",
                                "model": "deepseek-ai/DeepSeek-V3",
                            }
                        ],
                    }
                ),
                encoding="utf-8",
            )
            buffer = io.StringIO()

            with patch("sys.stdout", buffer):
                run_config_add_model_command(
                    config_path=config_path,
                    provider="siliconflow",
                    provider_id=None,
                    label=None,
                    api_key="sf-key-2",
                    model="deepseek-ai/DeepSeek-V3",
                    api_base=None,
                    set_default=False,
                )

            updated = json.loads(config_path.read_text(encoding="utf-8"))

        self.assertEqual(updated["providers"][-1]["id"], "deepseek-ai/DeepSeek-V3-2")

    def test_config_open_command_opens_synced_path(self) -> None:
        buffer = io.StringIO()
        with patch(
            "memnixa.gateway.cli.sync_default_config",
            return_value=(Path("/tmp/.memnixa/config.json"), True, []),
        ):
            with patch("memnixa.gateway.cli._open_path") as open_path:
                with patch("sys.stdout", buffer):
                    run_config_open_command()

        open_path.assert_called_once_with(Path("/tmp/.memnixa/config.json"))

    def test_data_dump_command_prints_sqlite_data(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            config = AppConfig(database_path=Path(temp_dir) / "memnixa.db")
            with patch("memnixa.gateway.cli.load_config", return_value=config):
                run_identity_bind_owner_command(
                    config_path=None,
                    channel="qq",
                    external_id="openid-1",
                )
                buffer = io.StringIO()
                with patch("sys.stdout", buffer):
                    run_data_dump_command(
                        config_path=None,
                        session_id=None,
                        limit=None,
                    )

        payload = json.loads(buffer.getvalue())
        self.assertIn("database_path", payload)
        self.assertIn("sessions", payload)
        self.assertIn("users", payload)
        self.assertIn("identities", payload)
        self.assertIn("messages", payload)


if __name__ == "__main__":
    unittest.main()
