"""Tests for main.py entry point, error handling, and integration."""

import asyncio
import os
import signal
import sys
from datetime import datetime, timezone
from io import StringIO
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from main import (
    AppFactoryError,
    ClarificationTimeout,
    ConfigurationError,
    DockerDaemonError,
    GracefulShutdown,
    GitError,
    MCPConnectionError,
    main,
    parse_args,
    print_summary,
    run_factory,
    validate_environment,
)


# ---------------------------------------------------------------------------
# parse_args tests
# ---------------------------------------------------------------------------


class TestParseArgs:
    def test_prompt_required(self):
        with pytest.raises(SystemExit):
            parse_args([])

    def test_prompt_only(self):
        args = parse_args(["--prompt", "Build a REST API"])
        assert args.prompt == "Build a REST API"
        assert args.repo_path == os.getcwd()
        assert args.max_concurrent_tasks == 5
        assert args.debug is False
        assert args.dry_run is False

    def test_all_options(self):
        args = parse_args([
            "--prompt", "Build an app",
            "--repo-path", "/tmp/project",
            "--max-concurrent-tasks", "3",
            "--debug",
            "--dry-run",
        ])
        assert args.prompt == "Build an app"
        assert args.repo_path == "/tmp/project"
        assert args.max_concurrent_tasks == 3
        assert args.debug is True
        assert args.dry_run is True

    def test_max_concurrent_tasks_default(self):
        args = parse_args(["--prompt", "test"])
        assert args.max_concurrent_tasks == 5

    def test_repo_path_default_is_cwd(self):
        args = parse_args(["--prompt", "test"])
        assert args.repo_path == os.getcwd()


# ---------------------------------------------------------------------------
# validate_environment tests
# ---------------------------------------------------------------------------


class TestValidateEnvironment:
    def test_valid_config(self):
        env = {
            "ANTHROPIC_API_KEY": "sk-test-key",
            "LANGSMITH_API_KEY": "ls-key",
            "LANGSMITH_PROJECT": "my-project",
        }
        with patch.dict(os.environ, env, clear=False), \
             patch("main.subprocess.run") as mock_run, \
             patch("main.shutil.which", return_value="/usr/bin/git"):
            mock_run.return_value = MagicMock(returncode=0)
            config = validate_environment()

        assert config["api_key"] == "sk-test-key"
        assert config["auth_token"] == ""
        assert config["langsmith_api_key"] == "ls-key"
        assert config["langsmith_project"] == "my-project"

    def test_missing_api_key_still_works(self):
        """API key is optional (Claude Code OAuth supported)."""
        with patch.dict(os.environ, {}, clear=True), \
             patch("main.subprocess.run") as mock_run, \
             patch("main.shutil.which", return_value="/usr/bin/git"):
            mock_run.return_value = MagicMock(returncode=0)
            config = validate_environment()
        assert config["api_key"] == ""
        assert config["auth_token"] == ""

    def test_docker_not_running(self):
        with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}, clear=False), \
             patch("main.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=1)
            with pytest.raises(DockerDaemonError, match="not running"):
                validate_environment()

    def test_docker_not_found(self):
        with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}, clear=False), \
             patch("main.subprocess.run", side_effect=FileNotFoundError):
            with pytest.raises(DockerDaemonError, match="not found"):
                validate_environment()

    def test_docker_timeout(self):
        import subprocess as sp

        with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}, clear=False), \
             patch("main.subprocess.run", side_effect=sp.TimeoutExpired("docker", 10)):
            with pytest.raises(DockerDaemonError, match="not responding"):
                validate_environment()

    def test_git_not_found(self):
        with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}, clear=False), \
             patch("main.subprocess.run") as mock_run, \
             patch("main.shutil.which", return_value=None):
            mock_run.return_value = MagicMock(returncode=0)
            with pytest.raises(GitError, match="git not found"):
                validate_environment()


# ---------------------------------------------------------------------------
# print_summary tests
# ---------------------------------------------------------------------------


class TestPrintSummary:
    def test_basic_summary(self, capsys):
        result = {
            "completed_tasks": ["1", "2"],
            "tasks": [{"id": 1}, {"id": 2}, {"id": 3}],
            "errors": [],
            "iteration_count": 5,
        }
        start = datetime.now(timezone.utc)
        print_summary(result, start)
        captured = capsys.readouterr().out
        assert "2 / 3" in captured
        assert "Iterations" in captured

    def test_summary_with_errors(self, capsys):
        result = {
            "completed_tasks": [],
            "tasks": [{"id": 1}],
            "errors": ["Error one", "Error two"],
            "iteration_count": 1,
        }
        start = datetime.now(timezone.utc)
        print_summary(result, start)
        captured = capsys.readouterr().out
        assert "Errors" in captured
        assert "Error one" in captured

    def test_summary_truncates_many_errors(self, capsys):
        result = {
            "completed_tasks": [],
            "tasks": [],
            "errors": [f"Error {i}" for i in range(10)],
            "iteration_count": 0,
        }
        start = datetime.now(timezone.utc)
        print_summary(result, start)
        captured = capsys.readouterr().out
        assert "and 5 more" in captured

    def test_summary_with_langsmith(self, capsys):
        result = {
            "completed_tasks": [],
            "tasks": [],
            "errors": [],
            "iteration_count": 0,
        }
        start = datetime.now(timezone.utc)
        with patch.dict(os.environ, {"LANGSMITH_PROJECT": "test-proj"}):
            print_summary(result, start)
        captured = capsys.readouterr().out
        assert "test-proj" in captured

    def test_summary_empty_result(self, capsys):
        result = {
            "completed_tasks": [],
            "tasks": [],
            "errors": [],
            "iteration_count": 0,
        }
        start = datetime.now(timezone.utc)
        print_summary(result, start)
        captured = capsys.readouterr().out
        assert "0 / 0" in captured


# ---------------------------------------------------------------------------
# GracefulShutdown tests
# ---------------------------------------------------------------------------


class TestGracefulShutdown:
    def test_initial_state(self):
        with patch("main.signal.signal"):
            gs = GracefulShutdown()
        assert gs.shutdown_requested is False
        assert gs.workspace_manager is None

    def test_registers_signals(self):
        with patch("main.signal.signal") as mock_signal:
            gs = GracefulShutdown()
        calls = [c[0] for c in mock_signal.call_args_list]
        assert (signal.SIGINT, gs._handler) in calls
        assert (signal.SIGTERM, gs._handler) in calls

    def test_first_signal_sets_flag(self):
        with patch("main.signal.signal"):
            gs = GracefulShutdown()
        # Simulate first signal
        with patch("builtins.print"):
            gs._handler(signal.SIGINT, None)
        assert gs.shutdown_requested is True

    def test_second_signal_force_exits(self):
        with patch("main.signal.signal"):
            gs = GracefulShutdown()
        gs.shutdown_requested = True
        with patch("builtins.print"), pytest.raises(SystemExit):
            gs._handler(signal.SIGINT, None)

    def test_first_signal_triggers_cleanup(self):
        mock_ws = MagicMock()
        mock_ws.cleanup_all = AsyncMock()
        with patch("main.signal.signal"):
            gs = GracefulShutdown(workspace_manager=mock_ws)

        # Simulate handler with a running loop
        loop = asyncio.new_event_loop()

        async def _run():
            with patch("builtins.print"):
                gs._handler(signal.SIGINT, None)

        loop.run_until_complete(_run())
        loop.close()
        assert gs.shutdown_requested is True


# ---------------------------------------------------------------------------
# run_factory tests
# ---------------------------------------------------------------------------


class TestRunFactory:
    @pytest.mark.asyncio
    async def test_initializes_all_components(self):
        mock_orchestrator_instance = MagicMock()
        mock_orchestrator_instance.run = AsyncMock(return_value={
            "completed_tasks": ["1"],
            "tasks": [{"id": 1}],
            "errors": [],
            "iteration_count": 1,
        })

        args = MagicMock()
        args.prompt = "Build a REST API"
        args.repo_path = "/tmp/test-repo"

        config = {
            "api_key": "sk-test-key",
            "auth_token": "",
            "langsmith_api_key": "",
            "langsmith_project": "app-factory",
        }

        with patch("app_factory.core.observability.ObservabilityManager") as mock_obs, \
             patch("app_factory.core.workspace.WorkspaceManager") as mock_ws, \
             patch("app_factory.core.architecture_tracker.ArchitectureTracker") as mock_arch, \
             patch("app_factory.agents.pm_agent.PMAgent") as mock_pm, \
             patch("app_factory.agents.task_agent.TaskMasterAgent") as mock_task, \
             patch("app_factory.agents.dev_agent.DevAgentManager") as mock_dev, \
             patch("app_factory.agents.qa_agent.QAAgent") as mock_qa, \
             patch("app_factory.core.graph.AppFactoryOrchestrator") as mock_orch, \
             patch("main.GracefulShutdown", create=True):
            mock_orch.return_value = mock_orchestrator_instance
            mock_ws.return_value = MagicMock(docker_client=MagicMock())

            result = await run_factory(args, config)

        assert result["completed_tasks"] == ["1"]
        mock_obs.assert_called_once()
        mock_ws.assert_called_once_with(repo_path="/tmp/test-repo")
        mock_arch.assert_called_once_with(
            api_key="sk-test-key",
            auth_token=None,
            debug=False,
            observability=mock_obs.return_value,
        )
        mock_pm.assert_called_once_with(
            api_key="sk-test-key",
            auth_token=None,
            debug=False,
            observability=mock_obs.return_value,
        )
        mock_task.assert_called_once_with(project_root="/tmp/test-repo")
        mock_dev.assert_called_once()
        mock_qa.assert_called_once_with(
            repo_path="/tmp/test-repo",
            api_key="sk-test-key",
            auth_token=None,
            debug=False,
            observability=mock_obs.return_value,
        )
        mock_orch.assert_called_once()
        mock_orchestrator_instance.run.assert_awaited_once_with("Build a REST API")


# ---------------------------------------------------------------------------
# main() integration tests
# ---------------------------------------------------------------------------


class TestMainEntryPoint:
    def test_dry_run_validates_without_executing(self):
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment") as mock_validate, \
             patch("builtins.print") as mock_print:
            mock_args.return_value = MagicMock(
                prompt="test", debug=False, dry_run=True,
            )
            mock_validate.return_value = {"api_key": "sk-test"}
            main()

        mock_print.assert_called_with(
            "Dry-run: configuration is valid. All checks passed."
        )

    def test_configuration_error_exits(self):
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment", side_effect=ConfigurationError("no key")), \
             patch("builtins.print"), \
             pytest.raises(SystemExit) as exc_info:
            mock_args.return_value = MagicMock(debug=False, dry_run=False)
            main()
        assert exc_info.value.code == 1

    def test_docker_error_exits(self):
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment", side_effect=DockerDaemonError("not running")), \
             patch("builtins.print"), \
             pytest.raises(SystemExit) as exc_info:
            mock_args.return_value = MagicMock(debug=False, dry_run=False)
            main()
        assert exc_info.value.code == 1

    def test_git_error_exits(self):
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment", side_effect=GitError("no git")), \
             patch("builtins.print"), \
             pytest.raises(SystemExit) as exc_info:
            mock_args.return_value = MagicMock(debug=False, dry_run=False)
            main()
        assert exc_info.value.code == 1

    def test_clarification_timeout_exits(self):
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment") as mock_validate, \
             patch("main.asyncio.run", side_effect=ClarificationTimeout("task 5")), \
             patch("builtins.print"), \
             pytest.raises(SystemExit) as exc_info:
            mock_args.return_value = MagicMock(
                prompt="test", debug=False, dry_run=False,
            )
            mock_validate.return_value = {"api_key": "sk-test"}
            main()
        assert exc_info.value.code == 1

    def test_generic_exception_exits(self):
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment") as mock_validate, \
             patch("main.asyncio.run", side_effect=RuntimeError("boom")), \
             patch("builtins.print"), \
             pytest.raises(SystemExit) as exc_info:
            mock_args.return_value = MagicMock(
                prompt="test", debug=False, dry_run=False,
            )
            mock_validate.return_value = {"api_key": "sk-test"}
            main()
        assert exc_info.value.code == 1

    def test_debug_flag_sets_logging(self):
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment") as mock_validate, \
             patch("main.logging.basicConfig") as mock_logging, \
             patch("builtins.print"):
            mock_args.return_value = MagicMock(
                prompt="test", debug=True, dry_run=True,
            )
            mock_validate.return_value = {"api_key": "sk-test"}
            main()

        mock_logging.assert_called_once()
        call_kwargs = mock_logging.call_args[1]
        assert call_kwargs["level"] == 10  # logging.DEBUG

    def test_successful_run(self):
        mock_result = {
            "completed_tasks": ["1"],
            "tasks": [{"id": 1}],
            "errors": [],
            "iteration_count": 3,
        }
        with patch("main.load_dotenv"), \
             patch("main.parse_args") as mock_args, \
             patch("main.validate_environment") as mock_validate, \
             patch("main.asyncio.run", return_value=mock_result), \
             patch("main.print_summary") as mock_summary:
            mock_args.return_value = MagicMock(
                prompt="test", debug=False, dry_run=False,
            )
            mock_validate.return_value = {"api_key": "sk-test"}
            main()

        mock_summary.assert_called_once()
        # Verify the result was passed to print_summary
        assert mock_summary.call_args[0][0] == mock_result


# ---------------------------------------------------------------------------
# Exception hierarchy tests
# ---------------------------------------------------------------------------


class TestExceptionHierarchy:
    def test_all_exceptions_inherit_from_base(self):
        assert issubclass(ClarificationTimeout, AppFactoryError)
        assert issubclass(DockerDaemonError, AppFactoryError)
        assert issubclass(GitError, AppFactoryError)
        assert issubclass(MCPConnectionError, AppFactoryError)
        assert issubclass(ConfigurationError, AppFactoryError)

    def test_base_inherits_from_exception(self):
        assert issubclass(AppFactoryError, Exception)
