"""
全面测试近期改动：
1. CLI 模式默认 -p 为当前目录
2. 直接运行模式保持时间戳默认
3. aacode 裸命令进入交互会话
4. aacode "task" 执行单任务后进入交互
5. aacode 不加 run 也等价于 run
6. 会话连续性：交互模式中多任务复用同一会话
7. 边界情况：-p/-t 组合、特殊字符、空任务等
"""
import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock, PropertyMock

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest


# ============================================================
# 1. CLI 分发逻辑 - 全路径覆盖
# ============================================================

class TestFullCliDispatch:
    """CLI 全路径分发测试"""

    def test_bare_calls_run_main_async_empty(self):
        from aacode import cli
        with patch.object(cli, "run_main_async") as m:
            with patch.object(sys, "argv", ["aacode"]):
                cli.main()
            m.assert_called_once_with([])

    def test_bare_with_dash_p_no_task(self):
        """aacode -p /tmp 无任务 → 传给 run_main_async"""
        from aacode import cli
        with patch.object(cli, "run_main_async") as m:
            with patch.object(sys, "argv", ["aacode", "-p", "/tmp/test"]):
                cli.main()
            m.assert_called_once_with(["-p", "/tmp/test"])

    def test_aacode_init_not_treated_as_run(self):
        from aacode import cli
        with patch.object(cli, "run_main_async") as m:
            with patch.object(cli, "is_pip_installed_mode", return_value=True):
                with patch.object(cli, "run_init_pip"):
                    with patch.object(sys, "argv", ["aacode", "init"]):
                        cli.main()
            m.assert_not_called()

    def test_aacode_run_with_task(self):
        from aacode import cli
        with patch.object(cli, "run_main_async") as m:
            with patch.object(sys, "argv", ["aacode", "run", "hello world"]):
                cli.main()
            m.assert_called_once_with(["hello world"])

    def test_aacode_run_with_p_and_task(self):
        from aacode import cli
        with patch.object(cli, "run_main_async") as m:
            with patch.object(sys, "argv", ["aacode", "run", "-p", "/x", "task"]):
                cli.main()
            m.assert_called_once_with(["-p", "/x", "task"])

    def test_unknown_first_arg_is_task(self):
        from aacode import cli
        with patch.object(cli, "run_main_async") as m:
            with patch.object(sys, "argv", ["aacode", "写一个helloworld程序"]):
                cli.main()
            m.assert_called_once_with(["写一个helloworld程序"])

    def test_help_short_flag(self):
        from aacode import cli
        with patch.object(cli, "run_main_async") as m:
            with patch("builtins.print"):
                with patch.object(sys, "argv", ["aacode", "-h"]):
                    cli.main()
            m.assert_not_called()


# ============================================================
# 2. run_main_async 正确设置 sys.argv
# ============================================================

class TestRunMainAsync:
    """run_main_async 设置 sys.argv 的测试"""

    def test_sets_argv0_to_aacode(self):
        from aacode.cli import run_main_async
        with patch("aacode.cli.asyncio.run") as mock_run:
            captured_argv = None

            def capture(coro):
                nonlocal captured_argv
                captured_argv = sys.argv[:]
                coro.close()  # 关闭未 await 的协程，避免 RuntimeWarning

            mock_run.side_effect = capture
            old_argv = sys.argv[:]
            run_main_async(["my", "args"])
            assert sys.argv == old_argv  # 恢复原始 argv

    def test_argv_restored_after_exception(self):
        from aacode.cli import run_main_async

        def close_and_raise(coro):
            coro.close()  # 关闭未 await 的协程，避免 RuntimeWarning
            raise RuntimeError("boom")

        old_argv = sys.argv[:]
        with patch("aacode.cli.asyncio.run", side_effect=close_and_raise):
            try:
                run_main_async(["args"])
            except RuntimeError:
                pass
        assert sys.argv == old_argv


# ============================================================
# 3. main.py - CLI 模式 vs 直接运行模式 默认值区分
# ============================================================

class TestCliModeDefault:
    """is_cli_mode 检测及 -p 默认值"""

    def test_cli_mode_default_is_dot(self):
        with patch.object(sys, "argv", ["aacode"]):
            is_cli = Path(sys.argv[0]).name == "aacode"
        assert is_cli is True

    def test_direct_mode_default_is_timestamped(self):
        with patch.object(sys, "argv", ["aacode/main.py"]):
            is_cli = Path(sys.argv[0]).name == "aacode"
        assert is_cli is False

    def test_absolute_main_py_not_cli(self):
        with patch.object(sys, "argv", ["/abs/path/main.py"]):
            is_cli = Path(sys.argv[0]).name == "aacode"
        assert is_cli is False

    def test_python_minus_m_aacode_is_cli(self):
        """python -m aacode 经过 __main__.py → cli.main() → sys.argv[0]='aacode'"""
        with patch.object(sys, "argv", ["aacode"]):
            is_cli = Path(sys.argv[0]).name == "aacode"
        assert is_cli is True

    def test_default_p_value_in_cli_mode(self):
        """验证 CLI 模式 argparse 默认值为 '.'"""
        import argparse
        with patch.object(sys, "argv", ["aacode"]):
            is_cli = Path(sys.argv[0]).name == "aacode"
            parser = argparse.ArgumentParser()
            parser.add_argument("-p", "--project",
                              default="." if is_cli else "projects/my_project_999")
            parser.add_argument("task", nargs="*")
            args = parser.parse_args([])
            assert args.project == "."

    def test_default_p_value_in_direct_mode(self):
        """验证直接运行模式 argparse 默认值为时间戳目录"""
        import argparse
        with patch.object(sys, "argv", ["main.py"]):
            is_cli = Path(sys.argv[0]).name == "aacode"
            parser = argparse.ArgumentParser()
            parser.add_argument("-p", "--project",
                              default="." if is_cli else "projects/my_project_999")
            parser.add_argument("task", nargs="*")
            args = parser.parse_args([])
            assert args.project == "projects/my_project_999"


# ============================================================
# 4. main() 空任务 → 交互模式
# ============================================================

class TestEmptyTaskInteractive:
    """空任务时直接进入 continue_session"""

    def test_no_task_goes_to_continue_session(self):
        from aacode import main as main_mod

        with patch.object(main_mod, "_setup_httpx_logging"):
            with patch.object(main_mod, "AICoder") as mock_ai:
                with patch.object(main_mod, "continue_session") as mock_continue:
                    with patch.object(sys, "argv", ["aacode", "-p", "/tmp"]):
                        import asyncio
                        loop = asyncio.new_event_loop()
                        try:
                            loop.run_until_complete(main_mod.main())
                        finally:
                            loop.close()

        mock_continue.assert_called_once()
        # AICoder 被创建（传入 continue_session 前）
        assert mock_ai.called

    def test_with_task_does_not_enter_continue_session_directly(self):
        """有任务时不跳过 coder.run"""
        from aacode import main as main_mod

        mock_coder = MagicMock()
        mock_coder.run = MagicMock()
        mock_coder.run.return_value = {
            "session_id": "test", "iterations": 0, "status": "ok", "execution_time": 0.0
        }

        with patch.object(main_mod, "_setup_httpx_logging"):
            with patch.object(main_mod, "AICoder", return_value=mock_coder):
                with patch.object(main_mod, "continue_session") as mock_continue:
                    with patch.object(sys, "argv", ["aacode", "-p", "/tmp", "a task"]):
                        with patch("builtins.input", return_value="n"):  # 回答不继续
                            import asyncio
                            loop = asyncio.new_event_loop()
                            try:
                                loop.run_until_complete(main_mod.main())
                            finally:
                                loop.close()

        mock_coder.run.assert_called_once_with("a task")

    def test_task_first_arg_is_dir_auto_detected(self):
        """第一个参数是存在的目录时自动识别为 target"""
        from aacode import main as main_mod

        with tempfile.TemporaryDirectory() as td:
            mock_coder = MagicMock()
            mock_coder.run = MagicMock()
            mock_coder.run.return_value = {
                "session_id": "t", "iterations": 1, "status": "ok", "execution_time": 1.0
            }

            with patch.object(main_mod, "_setup_httpx_logging"):
                with patch.object(main_mod, "AICoder", return_value=mock_coder):
                    with patch.object(main_mod, "continue_session"):
                        with patch.object(sys, "argv",
                                         ["aacode", "-p", "/tmp", td, "a task"]):
                            with patch("builtins.input", return_value="n"):
                                import asyncio
                                loop = asyncio.new_event_loop()
                                try:
                                    loop.run_until_complete(main_mod.main())
                                finally:
                                    loop.close()

            mock_coder.run.assert_called_once_with("a task")


# ============================================================
# 5. continue_session 会话连续性
# ============================================================

class TestSessionContinuity:
    """交互模式下多任务复用同一会话"""

    def test_continue_session_reuses_same_coder(self):
        """continue_session 多次调用 coder.run，复用同一 coder 实例"""
        from aacode.main import continue_session

        mock_coder = MagicMock()
        mock_coder.run = MagicMock()
        mock_coder.run.return_value = {
            "session_id": "s1", "iterations": 1, "status": "ok", "execution_time": 0.1
        }

        with tempfile.TemporaryDirectory() as td:
            project_dir = Path(td)
            (project_dir / ".aacode" / "todos").mkdir(parents=True, exist_ok=True)
            (project_dir / ".aacode" / "logs").mkdir(parents=True, exist_ok=True)

            with patch("builtins.input", side_effect=["task1", "task2", "exit"]):
                import asyncio
                loop = asyncio.new_event_loop()
                try:
                    loop.run_until_complete(continue_session(mock_coder, project_dir))
                finally:
                    loop.close()

            assert mock_coder.run.call_count == 2
            mock_coder.run.assert_any_call("task1")
            mock_coder.run.assert_any_call("task2")

    def test_continue_session_list_files(self):
        """在 continue_session 中输入 list 列出文件"""
        from aacode.main import continue_session

        mock_coder = MagicMock()
        with tempfile.TemporaryDirectory() as td:
            project_dir = Path(td)
            (project_dir / ".aacode" / "todos").mkdir(parents=True, exist_ok=True)
            (project_dir / ".aacode" / "logs").mkdir(parents=True, exist_ok=True)
            (project_dir / "hello.py").write_text("print('hello')")

            with patch("builtins.input", side_effect=["list", "exit"]):
                import asyncio
                loop = asyncio.new_event_loop()
                try:
                    loop.run_until_complete(continue_session(mock_coder, project_dir))
                finally:
                    loop.close()

            mock_coder.run.assert_not_called()

    def test_continue_session_quit_keywords(self):
        """exit / quit / q 都能退出"""
        from aacode.main import continue_session

        for quit_cmd in ["exit", "quit", "q"]:
            mock_coder = MagicMock()
            with tempfile.TemporaryDirectory() as td:
                project_dir = Path(td)
                (project_dir / ".aacode" / "todos").mkdir(parents=True, exist_ok=True)
                (project_dir / ".aacode" / "logs").mkdir(parents=True, exist_ok=True)

                with patch("builtins.input", side_effect=[quit_cmd]):
                    import asyncio
                    loop = asyncio.new_event_loop()
                    try:
                        loop.run_until_complete(continue_session(mock_coder, project_dir))
                    finally:
                        loop.close()

                mock_coder.run.assert_not_called()


# ============================================================
# 6. AICoder 初始化测试
# ============================================================

class TestAICoderInit:
    """AICoder 初始化行为测试"""

    def test_no_target_uses_project_as_target(self):
        from aacode.main import AICoder
        with tempfile.TemporaryDirectory() as td:
            coder = AICoder(td)
            assert coder.target_project == coder.project_path

    def test_with_target_uses_specified_target(self):
        from aacode.main import AICoder
        with tempfile.TemporaryDirectory() as td:
            project = Path(td) / "workspace"
            target = Path(td) / "target"
            target.mkdir()
            project.mkdir()
            coder = AICoder(str(project), target_project=str(target))
            assert coder.target_project == target.absolute()
            assert coder.project_path == project.absolute()

    def test_nonexistent_target_raises(self):
        from aacode.main import AICoder
        with tempfile.TemporaryDirectory() as td:
            with pytest.raises(ValueError, match="does not exist"):
                AICoder(td, target_project="/nonexistent/path")

    def test_permission_test_on_init(self):
        """初始化时检查写入权限"""
        from aacode.main import AICoder
        with tempfile.TemporaryDirectory() as td:
            coder = AICoder(td)
            # 不应抛出异常
            assert coder.project_path.exists()

    def test_aacode_dir_created(self):
        """初始化后 .aacode 目录应存在（ContextManager 会创建）"""
        from aacode.main import AICoder
        with tempfile.TemporaryDirectory() as td:
            coder = AICoder(td)
            assert coder.project_path.exists()


# ============================================================
# 7. 目标项目自动检测
# ============================================================

class TestTargetAutoDetection:
    """第一个位置参数如果是目录则自动识别为 target"""

    def test_short_dir_path_is_target(self):
        """长度 < 200 且存在 → 识别为 target"""
        import argparse
        from aacode.main import Path as MPath

        with tempfile.TemporaryDirectory() as td:
            parser = argparse.ArgumentParser()
            parser.add_argument("-p", "--project", default=".")
            parser.add_argument("task", nargs="*")
            args = parser.parse_args([td, "剩下的任务"])
            target = args.target if hasattr(args, 'target') else None
            task_parts = args.task

            if task_parts and not target:
                first = task_parts[0]
                if len(first) < 200 and MPath(first).is_dir():
                    target = first
                    task_parts = task_parts[1:]
            assert target == td
            assert task_parts == ["剩下的任务"]

    def test_long_string_not_treated_as_dir(self):
        """长度 >= 200 不识别为目录（避免长任务描述被误识别）"""
        import argparse
        from aacode.main import Path as MPath

        parser = argparse.ArgumentParser()
        parser.add_argument("-p", "--project", default=".")
        parser.add_argument("task", nargs="*")
        long_str = "a" * 200
        args = parser.parse_args([long_str, "task"])
        target = None
        task_parts = args.task

        if task_parts and not target:
            first = task_parts[0]
            if len(first) < 200 and MPath(first).is_dir():
                target = first
                task_parts = task_parts[1:]
        assert target is None
        assert task_parts == [long_str, "task"]

    def test_nonexistent_path_not_target(self):
        """不存在的路径不被识别为 target"""
        import argparse
        from aacode.main import Path as MPath

        parser = argparse.ArgumentParser()
        parser.add_argument("-p", "--project", default=".")
        parser.add_argument("task", nargs="*")
        args = parser.parse_args(["/nonexistent_xyz", "task"])
        target = None
        task_parts = args.task

        if task_parts and not target:
            first = task_parts[0]
            if len(first) < 200 and MPath(first).is_dir():
                target = first
                task_parts = task_parts[1:]
        assert target is None


# ============================================================
# 8. sys.argv 一致性
# ============================================================

class TestSysArgvConsistency:
    """整个调用链 sys.argv 正确性"""

    def test_main_receives_correct_argv_from_cli(self):
        """cli.main() → run_main_async → main() 收到正确的 sys.argv"""
        from aacode import cli

        captured_main_calls = []

        original_run = cli.run_main_async

        def fake_run(args):
            captured_main_calls.append(("run_main_async", sys.argv[:], args))

        cli.run_main_async = fake_run
        try:
            with patch.object(sys, "argv", ["aacode", "run", "-p", "/tmp", "task"]):
                cli.main()
        finally:
            cli.run_main_async = original_run

        assert len(captured_main_calls) == 1
        assert captured_main_calls[0][2] == ["-p", "/tmp", "task"]

    def test_bare_aacode_passes_empty_args(self):
        """bare aacode → run_main_async([])"""
        from aacode import cli

        captured = []
        original_run = cli.run_main_async

        def fake_run(args):
            captured.append(args)

        cli.run_main_async = fake_run
        try:
            with patch.object(sys, "argv", ["aacode"]):
                cli.main()
        finally:
            cli.run_main_async = original_run

        assert captured == [[]]
