from typing import Never
from unittest.mock import Mock, patch

import pytest
from rich.console import Console

from crackerjack.core.container import DependencyContainer
from crackerjack.core.session_coordinator import SessionCoordinator
from crackerjack.core.workflow_orchestrator import (
    WorkflowOrchestrator,
    WorkflowPipeline,
)
from crackerjack.models.config import WorkflowOptions


@pytest.fixture
def console():
    return Console()


@pytest.fixture
def pkg_path(tmp_path):
    return tmp_path


@pytest.fixture
def workflow_options():
    return WorkflowOptions()


class TestDependencyContainer:
    @pytest.fixture
    def container(self):
        return DependencyContainer()

    def test_init(self, container) -> None:
        assert hasattr(container, "_services")
        assert hasattr(container, "_singletons")
        assert isinstance(container._services, dict)
        assert isinstance(container._singletons, dict)

    def test_register_singleton(self, container) -> None:
        mock_service = Mock()

        container.register_singleton(str, mock_service)

        assert "str" in container._singletons
        assert container._singletons["str"] == mock_service

    def test_register_transient(self, container) -> None:
        mock_factory = Mock(return_value="test_service")

        container.register_transient(str, mock_factory)

        assert "str" in container._services
        assert container._services["str"] == mock_factory

    def test_get_singleton_service(self, container) -> None:
        mock_service = Mock()
        container.register_singleton(str, mock_service)

        service = container.get(str)

        assert service == mock_service

    def test_get_transient_service(self, container) -> None:
        mock_service = Mock()
        mock_factory = Mock(return_value=mock_service)
        container.register_transient(str, mock_factory)

        service = container.get(str)

        assert service == mock_service
        mock_factory.assert_called_once()

    def test_get_unregistered_service_raises_error(self, container) -> None:
        with pytest.raises(ValueError, match="Service str not registered"):
            container.get(str)

    def test_create_default_container(self, container, console, pkg_path) -> None:
        result = container.create_default_container(console, pkg_path, dry_run=False)

        assert result is container
        assert "FileSystemInterface" in container._singletons
        assert "GitInterface" in container._services
        assert "HookManager" in container._services
        assert "TestManagerProtocol" in container._services
        assert "PublishManager" in container._services


class TestSessionCoordinator:
    @pytest.fixture
    def session(self, console, pkg_path):
        return SessionCoordinator(console, pkg_path)

    def test_init(self, session, console, pkg_path) -> None:
        assert session.console == console
        assert session.pkg_path == pkg_path
        assert session.session_id is not None
        assert session.start_time is not None
        assert session.tasks == {}

    def test_start_session(self, session) -> None:
        task_name = "test_task"
        session.start_session(task_name)

        assert session.session_id is not None
        assert session.current_task == task_name

    def test_end_session_success(self, session) -> None:
        session.start_session("test_task")
        session.end_session(success=True)

        assert hasattr(session, "end_time")
        assert session.success is True

    def test_end_session_failure(self, session) -> None:
        session.start_session("test_task")
        session.end_session(success=False)

        assert hasattr(session, "end_time")
        assert session.success is False

    def test_track_task(self, session) -> None:
        session.start_session("main_task")

        task_id = session.track_task("subtask", "Subtask description")

        assert task_id is not None
        assert task_id in session.tasks
        assert session.tasks[task_id].task_id == "subtask"
        assert session.tasks[task_id].description == "Subtask description"

    def test_update_task_status(self, session) -> None:
        session.start_session("main_task")
        task_id = session.track_task("subtask", "Subtask description")

        session.update_task(task_id, "completed", details="Task completed successfully")

        task = session.tasks[task_id]
        assert task.status == "completed"
        assert task.details == "Task completed successfully"

    def test_update_task_progress(self, session) -> None:
        session.start_session("main_task")
        task_id = session.track_task("subtask", "Subtask description")

        session.update_task(task_id, "in_progress", progress=50)

        task = session.tasks[task_id]
        assert task.status == "in_progress"
        assert task.progress == 50

    def test_get_summary_empty(self, session) -> None:
        session.start_session("test_task")
        session.end_session(success=True)

        summary = session.get_summary()

        assert "session_id" in summary
        assert "duration" in summary
        assert "tasks_count" in summary
        assert summary["tasks_count"] == 0
        assert summary["success"] is True

    def test_get_summary_with_tasks(self, session) -> None:
        session.start_session("test_task")
        session.track_task("task1", "Task 1")
        session.track_task("task2", "Task 2")
        session.end_session(success=True)

        summary = session.get_summary()

        assert summary["tasks_count"] == 2
        assert "tasks" in summary
        assert len(summary["tasks"]) == 2

    def test_session_duration(self, session) -> None:
        session.start_session("test_task")

        with patch("time.time", return_value=session.start_time + 10):
            session.end_session(success=True)

        summary = session.get_summary()
        assert summary["duration"] >= 10

    def test_multiple_task_tracking(self, session) -> None:
        session.start_session("main_task")

        task1_id = session.track_task("task1", "First task")
        task2_id = session.track_task("task2", "Second task")
        task3_id = session.track_task("task3", "Third task")

        assert len(session.tasks) == 3
        assert task1_id != task2_id != task3_id

        session.update_task(task1_id, "completed")
        session.update_task(task2_id, "failed", details="Task failed")
        session.update_task(task3_id, "in_progress", progress=75)

        assert session.tasks[task1_id].status == "completed"
        assert session.tasks[task2_id].status == "failed"
        assert session.tasks[task3_id].status == "in_progress"
        assert session.tasks[task3_id].progress == 75


class TestWorkflowOrchestrator:
    @pytest.fixture
    def orchestrator(self, console, pkg_path):
        return WorkflowOrchestrator(console, pkg_path)

    def test_init(self, orchestrator, console, pkg_path) -> None:
        assert orchestrator.console == console
        assert orchestrator.pkg_path == pkg_path
        assert orchestrator.container is not None
        assert orchestrator.session is not None
        assert orchestrator.phases is not None

    @patch("crackerjack.core.workflow_orchestrator.version")
    def test_get_version_success(self, mock_version, orchestrator) -> None:
        mock_version.return_value = "1.0.0"

        version = orchestrator._get_version()

        assert version == "1.0.0"
        mock_version.assert_called_once()

    @patch("crackerjack.core.workflow_orchestrator.version")
    def test_get_version_fallback(self, mock_version, orchestrator) -> None:
        mock_version.side_effect = Exception("Version error")

        version = orchestrator._get_version()

        assert version == "unknown"

    async def test_process_clean_only(self, orchestrator, workflow_options) -> None:
        workflow_options.cleaning.clean = True

        with patch.object(orchestrator.session, "start_session") as mock_start:
            with patch.object(orchestrator.session, "end_session") as mock_end:
                with patch.object(
                    orchestrator,
                    "run_complete_workflow",
                    return_value=True,
                ) as mock_workflow:
                    result = await orchestrator.process(workflow_options)

                    mock_start.assert_called_once_with("process_workflow")
                    mock_workflow.assert_called_once_with(workflow_options)
                    mock_end.assert_called_once_with(success=True)
                    assert result is True

    async def test_process_with_hooks_and_tests(
        self,
        orchestrator,
        workflow_options,
    ) -> None:
        workflow_options.testing.test = True

        with patch.object(orchestrator.session, "start_session"):
            with patch.object(orchestrator.session, "end_session") as mock_end:
                with patch.object(
                    orchestrator,
                    "run_complete_workflow",
                    return_value=True,
                ) as mock_workflow:
                    result = await orchestrator.process(workflow_options)

                    mock_workflow.assert_called_once_with(workflow_options)
                    mock_end.assert_called_once_with(success=True)
                    assert result is True

    async def test_process_with_publishing(
        self,
        orchestrator,
        workflow_options,
    ) -> None:
        workflow_options.publishing.publish = "patch"

        with patch.object(orchestrator.session, "start_session"):
            with patch.object(orchestrator.session, "end_session") as mock_end:
                with patch.object(
                    orchestrator,
                    "run_complete_workflow",
                    return_value=True,
                ) as mock_workflow:
                    result = await orchestrator.process(workflow_options)

                    mock_workflow.assert_called_once_with(workflow_options)
                    mock_end.assert_called_once_with(success=True)
                    assert result is True

    async def test_process_with_commit(self, orchestrator, workflow_options) -> None:
        workflow_options.git.commit = True

        with patch.object(orchestrator.session, "start_session"):
            with patch.object(orchestrator.session, "end_session") as mock_end:
                with patch.object(
                    orchestrator,
                    "run_complete_workflow",
                    return_value=True,
                ) as mock_workflow:
                    result = await orchestrator.process(workflow_options)

                    mock_workflow.assert_called_once_with(workflow_options)
                    mock_end.assert_called_once_with(success=True)
                    assert result is True

    async def test_process_phase_failure(self, orchestrator, workflow_options) -> None:
        workflow_options.testing.test = True

        with patch.object(orchestrator.session, "start_session"):
            with patch.object(orchestrator.session, "end_session") as mock_end:
                with patch.object(
                    orchestrator,
                    "run_complete_workflow",
                    return_value=False,
                ) as mock_workflow:
                    result = await orchestrator.process(workflow_options)

                    mock_workflow.assert_called_once_with(workflow_options)
                    mock_end.assert_called_once_with(success=False)
                    assert result is False

    async def test_process_exception_handling(
        self,
        orchestrator,
        workflow_options,
    ) -> None:
        with patch.object(orchestrator.session, "start_session"):
            with patch.object(orchestrator.session, "end_session") as mock_end:
                with patch.object(
                    orchestrator,
                    "run_complete_workflow",
                    side_effect=Exception("Test error"),
                ) as mock_workflow:
                    result = await orchestrator.process(workflow_options)

                    mock_workflow.assert_called_once_with(workflow_options)
                    mock_end.assert_called_once_with(success=False)
                    assert result is False


class TestWorkflowPipeline:
    @pytest.fixture
    def pipeline(self, console, pkg_path):
        from unittest.mock import Mock

        session = Mock()
        phases = Mock()
        return WorkflowPipeline(console, pkg_path, session, phases)

    def test_init(self, pipeline, console, pkg_path) -> None:
        assert pipeline.console == console
        assert pipeline.pkg_path == pkg_path
        assert pipeline.session is not None
        assert pipeline.phases is not None

    async def test_execute_success(self, pipeline, workflow_options) -> None:
        async def mock_workflow(options) -> bool:
            return True

        with patch.object(
            pipeline,
            "run_complete_workflow",
            side_effect=mock_workflow,
        ) as mock_run:
            result = await pipeline.run_complete_workflow(workflow_options)

            mock_run.assert_called_once_with(workflow_options)
            assert result is True

    async def test_execute_with_exception(self, pipeline, workflow_options) -> None:
        async def mock_workflow(options) -> Never:
            msg = "Test error"
            raise Exception(msg)

        with patch.object(
            pipeline,
            "run_complete_workflow",
            side_effect=mock_workflow,
        ) as mock_run:
            try:
                result = await pipeline.run_complete_workflow(workflow_options)
            except Exception:
                result = False

            mock_run.assert_called_once_with(workflow_options)
            assert result is False

    async def test_execute_with_keyboard_interrupt(
        self,
        pipeline,
        workflow_options,
    ) -> None:
        async def mock_workflow(options) -> Never:
            raise KeyboardInterrupt

        with patch.object(
            pipeline,
            "run_complete_workflow",
            side_effect=mock_workflow,
        ) as mock_run:
            try:
                result = await pipeline.run_complete_workflow(workflow_options)
            except KeyboardInterrupt:
                result = False

            mock_run.assert_called_once_with(workflow_options)
            assert result is False

    async def test_execute_workflow_options_forwarding(self, pipeline) -> None:
        options = WorkflowOptions()
        options.cleaning.clean = True
        options.testing.test = True
        options.execution.verbose = True

        async def mock_workflow(opts) -> bool:
            return True

        with patch.object(
            pipeline,
            "run_complete_workflow",
            side_effect=mock_workflow,
        ) as mock_run:
            await pipeline.run_complete_workflow(options)

            mock_run.assert_called_once_with(options)
            passed_options = mock_run.call_args[0][0]
            assert passed_options.cleaning.clean is True
            assert passed_options.testing.test is True
            assert passed_options.execution.verbose is True
