"""
Tests for ExecutionContext and ProgressReporter classes.
"""

from datetime import datetime, timezone

import pytest

from torivers_sdk.context.execution import ExecutionContext
from torivers_sdk.context.progress import (
    ConsoleProgressReporter,
    LogLevel,
    MockProgressReporter,
    ProgressEntry,
    ProgressReporter,
    ProgressReporterBase,
)


class TestLogLevel:
    """Test suite for LogLevel enum."""

    def test_log_levels_exist(self) -> None:
        """Test that all expected log levels exist."""
        assert LogLevel.INFO == "info"
        assert LogLevel.ACTION == "action"
        assert LogLevel.SUCCESS == "success"
        assert LogLevel.WARNING == "warning"
        assert LogLevel.ERROR == "error"

    def test_log_level_is_string_enum(self) -> None:
        """Test that LogLevel values are strings."""
        assert isinstance(LogLevel.INFO.value, str)
        assert LogLevel.SUCCESS.value == "success"


class TestProgressEntry:
    """Test suite for ProgressEntry dataclass."""

    def test_create_entry(self) -> None:
        """Test creating a progress entry."""
        entry = ProgressEntry(
            level=LogLevel.ACTION,
            title="Processing",
            description="Step 1",
        )
        assert entry.level == LogLevel.ACTION
        assert entry.title == "Processing"
        assert entry.description == "Step 1"
        assert isinstance(entry.timestamp, datetime)

    def test_entry_has_utc_timestamp(self) -> None:
        """Test that entry timestamp is UTC."""
        entry = ProgressEntry(
            level=LogLevel.INFO,
            title="Test",
            description="",
        )
        assert entry.timestamp.tzinfo == timezone.utc

    def test_entry_with_metadata(self) -> None:
        """Test creating entry with metadata."""
        entry = ProgressEntry(
            level=LogLevel.SUCCESS,
            title="Done",
            description="Completed",
            metadata={"count": 100, "source": "api"},
        )
        assert entry.metadata["count"] == 100
        assert entry.metadata["source"] == "api"

    def test_to_dict(self) -> None:
        """Test converting entry to dictionary."""
        entry = ProgressEntry(
            level=LogLevel.WARNING,
            title="Rate limit",
            description="Approaching limit",
            metadata={"remaining": 10},
        )
        data = entry.to_dict()

        assert data["level"] == "warning"
        assert data["title"] == "Rate limit"
        assert data["description"] == "Approaching limit"
        assert "timestamp" in data
        assert data["metadata"]["remaining"] == 10


class TestProgressReporter:
    """Test suite for ProgressReporter class."""

    def test_create_reporter(self) -> None:
        """Test creating a progress reporter."""
        reporter = ProgressReporter(execution_id="exec-123")
        assert reporter.execution_id == "exec-123"
        assert len(reporter.entries) == 0

    def test_log_info(self) -> None:
        """Test logging info message."""
        reporter = ProgressReporter()
        reporter.log_info("Configuration loaded", "Using default settings")

        assert len(reporter.entries) == 1
        entry = reporter.entries[0]
        assert entry.level == LogLevel.INFO
        assert entry.title == "Configuration loaded"
        assert entry.description == "Using default settings"

    def test_log_action(self) -> None:
        """Test logging action message."""
        reporter = ProgressReporter()
        reporter.log_action("Processing", "Step 1 of 3")

        assert len(reporter.entries) == 1
        entry = reporter.entries[0]
        assert entry.level == LogLevel.ACTION
        assert entry.title == "Processing"

    def test_log_success(self) -> None:
        """Test logging success message."""
        reporter = ProgressReporter()
        reporter.log_success("Complete", "All items processed")

        assert len(reporter.entries) == 1
        entry = reporter.entries[0]
        assert entry.level == LogLevel.SUCCESS

    def test_log_warning(self) -> None:
        """Test logging warning message."""
        reporter = ProgressReporter()
        reporter.log_warning("Slow response", "API is slow")

        assert len(reporter.entries) == 1
        entry = reporter.entries[0]
        assert entry.level == LogLevel.WARNING

    def test_log_error(self) -> None:
        """Test logging error message."""
        reporter = ProgressReporter()
        reporter.log_error("Failed", "Connection timeout")

        assert len(reporter.entries) == 1
        entry = reporter.entries[0]
        assert entry.level == LogLevel.ERROR

    def test_log_with_metadata(self) -> None:
        """Test logging with metadata."""
        reporter = ProgressReporter()
        reporter.log_action(
            "Processing items",
            "Batch processing",
            metadata={"batch_size": 100, "current_batch": 1},
        )

        entry = reporter.entries[0]
        assert entry.metadata["batch_size"] == 100
        assert entry.metadata["current_batch"] == 1

    def test_callback(self) -> None:
        """Test callback is called for each log."""
        logged_entries: list[ProgressEntry] = []
        reporter = ProgressReporter(callback=logged_entries.append)

        reporter.log_action("Step 1", "")
        reporter.log_success("Done", "")

        assert len(logged_entries) == 2
        assert logged_entries[0].title == "Step 1"
        assert logged_entries[1].title == "Done"

    def test_clear_entries(self) -> None:
        """Test clearing logged entries."""
        reporter = ProgressReporter()
        reporter.log_action("Test", "")
        reporter.log_success("Done", "")

        assert len(reporter.entries) == 2
        reporter.clear()
        assert len(reporter.entries) == 0

    def test_get_entries_by_level(self) -> None:
        """Test filtering entries by level."""
        reporter = ProgressReporter()
        reporter.log_action("Action 1", "")
        reporter.log_success("Success 1", "")
        reporter.log_action("Action 2", "")
        reporter.log_error("Error 1", "")

        actions = reporter.get_entries_by_level(LogLevel.ACTION)
        assert len(actions) == 2

        errors = reporter.get_entries_by_level(LogLevel.ERROR)
        assert len(errors) == 1

    def test_has_errors(self) -> None:
        """Test checking for errors."""
        reporter = ProgressReporter()
        assert not reporter.has_errors()

        reporter.log_action("Processing", "")
        assert not reporter.has_errors()

        reporter.log_error("Failed", "")
        assert reporter.has_errors()

    def test_has_warnings(self) -> None:
        """Test checking for warnings."""
        reporter = ProgressReporter()
        assert not reporter.has_warnings()

        reporter.log_warning("Slow", "")
        assert reporter.has_warnings()


class TestMockProgressReporter:
    """Test suite for MockProgressReporter class."""

    def test_create_mock_reporter(self) -> None:
        """Test creating a mock reporter."""
        reporter = MockProgressReporter()
        assert len(reporter.entries) == 0

    def test_count_properties(self) -> None:
        """Test count properties."""
        reporter = MockProgressReporter()
        reporter.log_info("Info", "")
        reporter.log_action("Action 1", "")
        reporter.log_action("Action 2", "")
        reporter.log_success("Success", "")
        reporter.log_warning("Warning", "")
        reporter.log_error("Error", "")

        assert reporter.info_count == 1
        assert reporter.action_count == 2
        assert reporter.success_count == 1
        assert reporter.warning_count == 1
        assert reporter.error_count == 1

    def test_has_entry_with_title(self) -> None:
        """Test checking for entry by title."""
        reporter = MockProgressReporter()
        reporter.log_action("Processing data", "")

        assert reporter.has_entry_with_title("Processing data")
        assert not reporter.has_entry_with_title("Missing title")

    def test_get_last_entry(self) -> None:
        """Test getting last entry."""
        reporter = MockProgressReporter()
        assert reporter.get_last_entry() is None

        reporter.log_action("First", "")
        reporter.log_success("Second", "")

        last = reporter.get_last_entry()
        assert last is not None
        assert last.title == "Second"

    def test_assert_no_errors(self) -> None:
        """Test assert_no_errors method."""
        reporter = MockProgressReporter()
        reporter.log_action("Processing", "")
        reporter.log_success("Done", "")

        # Should not raise
        reporter.assert_no_errors()

        reporter.log_error("Failed", "")
        with pytest.raises(AssertionError, match="Expected no errors"):
            reporter.assert_no_errors()

    def test_assert_has_entry(self) -> None:
        """Test assert_has_entry method."""
        reporter = MockProgressReporter()
        reporter.log_action("Processing", "Step 1")

        # Should not raise
        reporter.assert_has_entry(LogLevel.ACTION, "Processing")
        reporter.assert_has_entry(LogLevel.ACTION, "Processing", "Step 1")

        # Should raise
        with pytest.raises(AssertionError):
            reporter.assert_has_entry(LogLevel.SUCCESS, "Processing")

        with pytest.raises(AssertionError):
            reporter.assert_has_entry(LogLevel.ACTION, "Wrong title")

    def test_mock_with_metadata(self) -> None:
        """Test mock reporter handles metadata."""
        reporter = MockProgressReporter()
        reporter.log_action("Process", "", metadata={"count": 10})

        entry = reporter.entries[0]
        assert entry.metadata["count"] == 10


class TestConsoleProgressReporter:
    """Test suite for ConsoleProgressReporter class."""

    def test_create_console_reporter(self) -> None:
        """Test creating a console reporter."""
        reporter = ConsoleProgressReporter(use_colors=False)
        assert len(reporter.entries) == 0

    def test_logs_to_entries(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Test that logs are stored in entries."""
        reporter = ConsoleProgressReporter(use_colors=False)
        reporter.log_action("Processing", "Details")

        assert len(reporter.entries) == 1
        assert reporter.entries[0].title == "Processing"

        # Check console output
        captured = capsys.readouterr()
        assert "Processing" in captured.out

    def test_prints_description(self, capsys: pytest.CaptureFixture[str]) -> None:
        """Test that description is printed."""
        reporter = ConsoleProgressReporter(use_colors=False)
        reporter.log_success("Done", "All 100 items processed")

        captured = capsys.readouterr()
        assert "Done" in captured.out
        assert "All 100 items processed" in captured.out

    def test_clear(self) -> None:
        """Test clearing entries."""
        reporter = ConsoleProgressReporter(use_colors=False)
        reporter.log_action("Test", "")
        reporter.clear()
        assert len(reporter.entries) == 0


class TestExecutionContext:
    """Test suite for ExecutionContext class."""

    def test_create_context(self) -> None:
        """Test creating execution context."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        assert context.execution_id == "exec-123"
        assert context.progress is reporter

    def test_log_info(self) -> None:
        """Test logging info through context."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.log_info("Config loaded", "Using defaults")

        assert reporter.info_count == 1
        entry = reporter.entries[0]
        assert entry.level == LogLevel.INFO
        assert entry.title == "Config loaded"

    def test_log_progress(self) -> None:
        """Test logging progress through context."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.log_progress("Processing", "Step 1 of 3")

        assert reporter.action_count == 1
        entry = reporter.entries[0]
        assert entry.level == LogLevel.ACTION
        assert entry.title == "Processing"

    def test_log_success(self) -> None:
        """Test logging success through context."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.log_success("Complete", "100 items processed")

        assert reporter.success_count == 1

    def test_log_warning(self) -> None:
        """Test logging warning through context."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.log_warning("Rate limit", "Approaching limit")

        assert reporter.warning_count == 1

    def test_log_error(self) -> None:
        """Test logging error through context."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.log_error("Failed", "Connection timeout")

        assert reporter.error_count == 1

    def test_log_with_metadata(self) -> None:
        """Test logging with metadata through context."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.log_progress(
            "Processing batch",
            "Batch 1 of 10",
            metadata={"batch_size": 100},
        )

        entry = reporter.entries[0]
        assert entry.metadata["batch_size"] == 100

    def test_set_get_metadata(self) -> None:
        """Test setting and getting execution metadata."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.set_metadata("items_processed", 100)
        context.set_metadata("source", "api")

        assert context.get_metadata("items_processed") == 100
        assert context.get_metadata("source") == "api"
        assert context.get_metadata("missing") is None
        assert context.get_metadata("missing", "default") == "default"

    def test_get_all_metadata(self) -> None:
        """Test getting all metadata."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.set_metadata("key1", "value1")
        context.set_metadata("key2", "value2")

        all_metadata = context.get_all_metadata()
        assert all_metadata == {"key1": "value1", "key2": "value2"}

        # Verify it's a copy
        all_metadata["key3"] = "value3"
        assert context.get_metadata("key3") is None

    def test_clear_metadata(self) -> None:
        """Test clearing metadata."""
        reporter = MockProgressReporter()
        context = ExecutionContext("exec-123", reporter)

        context.set_metadata("key", "value")
        assert context.get_metadata("key") == "value"

        context.clear_metadata()
        assert context.get_metadata("key") is None

    def test_log_forwards_to_action_client(self) -> None:
        """Test context logs are forwarded to action client when available."""

        class _MockActions:
            def __init__(self) -> None:
                self.calls: list[tuple[str, dict]] = []

            def execute(self, action: str, payload: dict) -> dict:
                self.calls.append((action, payload))
                return {"ok": True}

        reporter = MockProgressReporter()
        mock_actions = _MockActions()
        context = ExecutionContext("exec-123", reporter, actions=mock_actions)  # type: ignore[arg-type]

        context.log_info("Config loaded", "Using defaults", {"env": "test"})

        assert reporter.info_count == 1
        assert len(mock_actions.calls) == 1
        action, payload = mock_actions.calls[0]
        assert action == "log.write"
        assert payload["level"] == "info"
        assert payload["title"] == "Config loaded"


class TestProgressReporterInterface:
    """Test that all reporters implement the interface."""

    def test_progress_reporter_is_base_instance(self) -> None:
        """Test ProgressReporter is a ProgressReporterBase."""
        reporter = ProgressReporter()
        assert isinstance(reporter, ProgressReporterBase)

    def test_mock_reporter_is_base_instance(self) -> None:
        """Test MockProgressReporter is a ProgressReporterBase."""
        reporter = MockProgressReporter()
        assert isinstance(reporter, ProgressReporterBase)

    def test_console_reporter_is_base_instance(self) -> None:
        """Test ConsoleProgressReporter is a ProgressReporterBase."""
        reporter = ConsoleProgressReporter()
        assert isinstance(reporter, ProgressReporterBase)

    def test_all_reporters_have_required_methods(self) -> None:
        """Test all reporters have the required methods."""
        reporters: list[ProgressReporterBase] = [
            ProgressReporter(),
            MockProgressReporter(),
            ConsoleProgressReporter(use_colors=False),
        ]

        for reporter in reporters:
            # All should have these methods
            assert callable(getattr(reporter, "log_info"))
            assert callable(getattr(reporter, "log_action"))
            assert callable(getattr(reporter, "log_success"))
            assert callable(getattr(reporter, "log_warning"))
            assert callable(getattr(reporter, "log_error"))
