"""Unit tests for PentestProgressDisplay."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from ziran.application.agent_scanner.scanner import ProgressEvent, ProgressEventType
from ziran.application.pentesting.progress import PentestProgressDisplay

# ──────────────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────────────


def _make_console() -> MagicMock:
    """Return a mock Rich Console that captures print calls."""
    console = MagicMock()
    return console


def _make_event(
    event: ProgressEventType,
    *,
    phase: str = "prompt_injection",
    phase_index: int = 0,
    total_phases: int = 3,
    attack_index: int = 0,
    total_attacks: int = 5,
    attack_name: str = "PI Basic",
    message: str = "",
    extra: dict | None = None,
) -> ProgressEvent:
    return ProgressEvent(
        event=event,
        phase=phase,
        phase_index=phase_index,
        total_phases=total_phases,
        attack_index=attack_index,
        total_attacks=total_attacks,
        attack_name=attack_name,
        message=message,
        extra=extra or {},
    )


# ──────────────────────────────────────────────────────────────────────
# Agent event callback
# ──────────────────────────────────────────────────────────────────────


@pytest.mark.unit
class TestAgentEventCallback:
    """Tests for on_agent_event callback."""

    def test_plan_event_prints_header_and_message(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)
        display.on_agent_event("plan", "Analyzing target capabilities")

        # Header rule should be shown on first event
        console.rule.assert_called_once()
        assert console.print.call_count >= 2  # header + event line

    def test_execute_event(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)
        display._header_shown = True
        display.on_agent_event("execute", "Running prompt injection phase")

        console.print.assert_called_once()
        printed = console.print.call_args[0][0]
        assert "EXECUTE" in printed

    def test_reason_event(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)
        display._header_shown = True
        display.on_agent_event("reason", "Found 2 vulnerabilities")

        printed = console.print.call_args[0][0]
        assert "REASON" in printed

    def test_report_event(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)
        display._header_shown = True
        display.on_agent_event("report", "Generating final report")

        printed = console.print.call_args[0][0]
        assert "REPORT" in printed

    def test_unknown_event_uses_default_style(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)
        display._header_shown = True
        display.on_agent_event("custom_event", "Something happened")

        printed = console.print.call_args[0][0]
        assert "CUSTOM_EVENT" in printed

    def test_header_shown_only_once(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        display.on_agent_event("plan", "First event")
        display.on_agent_event("execute", "Second event")

        assert console.rule.call_count == 1

    def test_no_output_when_rich_unavailable(self) -> None:
        display = PentestProgressDisplay()
        display._console = None
        # Should not raise
        display.on_agent_event("plan", "This should be silently ignored")


# ──────────────────────────────────────────────────────────────────────
# Scan progress callback
# ──────────────────────────────────────────────────────────────────────


@pytest.mark.unit
class TestProgressCallback:
    """Tests for on_progress callback (scan-level events)."""

    def test_phase_start_prints_phase_name(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(ProgressEventType.PHASE_START, phase="prompt_injection")
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "PHASE" in printed
        assert "Prompt Injection" in printed

    def test_phase_attacks_loaded_verbose(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console, verbose=True)

        event = _make_event(ProgressEventType.PHASE_ATTACKS_LOADED, total_attacks=12)
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "12" in printed

    def test_phase_attacks_loaded_silent_by_default(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(ProgressEventType.PHASE_ATTACKS_LOADED, total_attacks=12)
        display.on_progress(event)

        console.print.assert_not_called()

    def test_attack_start_verbose(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console, verbose=True)

        event = _make_event(
            ProgressEventType.ATTACK_START,
            attack_index=2,
            total_attacks=10,
            attack_name="Injection Test Alpha",
        )
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "3/10" in printed  # 1-indexed display

    def test_attack_start_silent_by_default(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(ProgressEventType.ATTACK_START)
        display.on_progress(event)

        console.print.assert_not_called()

    def test_attack_complete_successful_prints_hit(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(
            ProgressEventType.ATTACK_COMPLETE,
            attack_name="Jailbreak Payload",
            extra={"successful": True, "severity": "high"},
        )
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "HIT!" in printed
        assert "Jailbreak Payload" in printed
        assert display._vulns_found == 1

    def test_attack_complete_blocked_silent_by_default(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(
            ProgressEventType.ATTACK_COMPLETE,
            extra={"successful": False},
        )
        display.on_progress(event)

        console.print.assert_not_called()

    def test_attack_complete_blocked_verbose(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console, verbose=True)

        event = _make_event(
            ProgressEventType.ATTACK_COMPLETE,
            attack_index=3,
            total_attacks=10,
            extra={"successful": False},
        )
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "blocked" in printed

    def test_phase_complete_with_vulns(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(
            ProgressEventType.PHASE_COMPLETE,
            extra={"vulnerabilities_found": 3, "attacks_executed": 15},
        )
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "DONE" in printed
        assert "3" in printed
        assert "vulns" in printed

    def test_phase_complete_no_vulns(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(
            ProgressEventType.PHASE_COMPLETE,
            extra={"vulnerabilities_found": 0, "attacks_executed": 10},
        )
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "0 vulns" in printed

    def test_campaign_complete_ignored(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(ProgressEventType.CAMPAIGN_COMPLETE)
        display.on_progress(event)

        console.print.assert_not_called()

    def test_no_output_when_rich_unavailable(self) -> None:
        display = PentestProgressDisplay()
        display._console = None

        event = _make_event(ProgressEventType.PHASE_START)
        display.on_progress(event)  # Should not raise


# ──────────────────────────────────────────────────────────────────────
# Summary table
# ──────────────────────────────────────────────────────────────────────


@pytest.mark.unit
class TestShowSummary:
    """Tests for show_summary."""

    def test_summary_prints_table(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        display.show_summary(
            session_id="abc123",
            status="complete",
            iterations=5,
            vulnerabilities=3,
            attacks=20,
            findings=2,
            tokens=5000,
        )

        # Should print blank line + table + blank line = at least 3 calls
        assert console.print.call_count >= 3
        # The Table object should be in one of the print calls
        from rich.table import Table

        table_calls = [
            c for c in console.print.call_args_list if c[0] and isinstance(c[0][0], Table)
        ]
        assert len(table_calls) == 1

    def test_summary_no_output_without_console(self) -> None:
        display = PentestProgressDisplay()
        display._console = None

        # Should not raise
        display.show_summary(
            session_id="abc",
            status="complete",
            iterations=1,
            vulnerabilities=0,
            attacks=5,
            findings=0,
            tokens=100,
        )


# ──────────────────────────────────────────────────────────────────────
# Severity color mapping
# ──────────────────────────────────────────────────────────────────────


@pytest.mark.unit
class TestSeverityColors:
    """Tests for severity-based coloring in attack complete events."""

    @pytest.mark.parametrize(
        "severity",
        ["critical", "high", "medium", "low"],
    )
    def test_known_severities_are_styled(self, severity: str) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(
            ProgressEventType.ATTACK_COMPLETE,
            attack_name="Test Attack",
            extra={"successful": True, "severity": severity},
        )
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert severity in printed

    def test_unknown_severity_uses_default(self) -> None:
        console = _make_console()
        display = PentestProgressDisplay(console=console)

        event = _make_event(
            ProgressEventType.ATTACK_COMPLETE,
            attack_name="Test Attack",
            extra={"successful": True, "severity": "info"},
        )
        display.on_progress(event)

        printed = console.print.call_args[0][0]
        assert "Test Attack" in printed


# ──────────────────────────────────────────────────────────────────────
# Orchestrator integration — show_progress wiring
# ──────────────────────────────────────────────────────────────────────


@pytest.mark.unit
class TestOrchestratorShowProgress:
    """Verify the orchestrator wires up the progress display."""

    def test_show_progress_true_creates_display(self) -> None:
        from ziran.application.pentesting.orchestrator import PentestOrchestrator

        orchestrator = PentestOrchestrator(
            adapter=MagicMock(),
            llm_client=MagicMock(complete=AsyncMock()),
            show_progress=True,
        )
        assert orchestrator._progress_display is not None

    def test_show_progress_false_no_display(self) -> None:
        from ziran.application.pentesting.orchestrator import PentestOrchestrator

        orchestrator = PentestOrchestrator(
            adapter=MagicMock(),
            llm_client=MagicMock(complete=AsyncMock()),
            show_progress=False,
        )
        assert orchestrator._progress_display is None

    def test_custom_callbacks_override_display(self) -> None:
        from ziran.application.pentesting.orchestrator import PentestOrchestrator

        custom_progress = MagicMock()
        custom_agent_event = MagicMock()

        orchestrator = PentestOrchestrator(
            adapter=MagicMock(),
            llm_client=MagicMock(complete=AsyncMock()),
            on_progress=custom_progress,
            on_agent_event=custom_agent_event,
            show_progress=True,  # should be ignored
        )
        assert orchestrator._progress_display is None

    async def test_run_autonomous_calls_show_summary(self) -> None:
        from ziran.application.pentesting.orchestrator import PentestOrchestrator
        from ziran.domain.entities.pentest import PentestSession, PentestStatus

        orchestrator = PentestOrchestrator(
            adapter=MagicMock(),
            llm_client=MagicMock(complete=AsyncMock()),
        )

        session = PentestSession(status=PentestStatus.COMPLETE, max_iterations=5)
        session.total_vulnerabilities = 2
        session.total_attacks_executed = 10

        orchestrator.agent.run = AsyncMock(return_value=session)
        orchestrator.scanner._attack_results = []
        orchestrator._progress_display = MagicMock()

        await orchestrator.run_autonomous(goal="Test")

        orchestrator._progress_display.show_summary.assert_called_once()
        call_kwargs = orchestrator._progress_display.show_summary.call_args[1]
        assert call_kwargs["status"] == "complete"
        assert call_kwargs["vulnerabilities"] == 2
        assert call_kwargs["attacks"] == 10
