"""Pytest plugin for visual regression testing.

Provides a `visual` fixture that makes visual regression testing trivial.

Usage:
    # In your test
    def test_homepage(page, visual):
        page.goto("/")
        visual.snapshot("homepage")

        page.click("button.login")
        visual.snapshot("login-modal")

    # Run with update flag to accept new baselines
    pytest --visual-update

    # Run with specific threshold
    pytest --visual-threshold=0.05

Configuration in pytest.ini:
    [pytest]
    visual_baselines_dir = baselines
    visual_default_threshold = 0.1
    visual_fail_on_missing = false
"""
from __future__ import annotations

import logging
import os
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional

import pytest

from autodebug.visual.capture import AsyncVisualCapture, VisualCapture, VisualSnapshot
from autodebug.visual.diff import DiffStatus
from autodebug.visual.storage import (
    BaselineStorage,
    ComparisonResult,
    get_storage,
)

if TYPE_CHECKING:
    from playwright.sync_api import Page as PlaywrightPage

logger = logging.getLogger(__name__)


def pytest_addoption(parser):
    """Add visual regression CLI options."""
    group = parser.getgroup("visual", "Visual Regression Testing")

    group.addoption(
        "--visual-update",
        action="store_true",
        default=False,
        help="Update baselines with current snapshots (approve all changes)",
    )

    group.addoption(
        "--visual-threshold",
        type=float,
        default=0.1,
        help="Diff threshold (0.0-1.0). Lower = stricter. Default: 0.1",
    )

    group.addoption(
        "--visual-fail-on-missing",
        action="store_true",
        default=False,
        help="Fail if baseline is missing (instead of creating it)",
    )

    group.addoption(
        "--visual-baselines-dir",
        type=str,
        default="baselines",
        help="Directory for baseline images. Default: baselines",
    )

    group.addoption(
        "--visual-storage",
        type=str,
        default="auto",
        choices=["auto", "local", "supabase"],
        help="Storage backend for baselines. Default: auto",
    )


def pytest_configure(config):
    """Register visual marker."""
    config.addinivalue_line(
        "markers",
        "visual: mark test as a visual regression test",
    )


@dataclass
class VisualTestResult:
    """Result of a visual test run."""

    test_name: str
    comparisons: List[ComparisonResult] = field(default_factory=list)
    passed: bool = True
    errors: List[str] = field(default_factory=list)

    @property
    def needs_review(self) -> bool:
        return any(c.needs_review for c in self.comparisons)

    @property
    def diff_count(self) -> int:
        return sum(1 for c in self.comparisons if c.diff_result.status == DiffStatus.DIFF)

    @property
    def new_count(self) -> int:
        return sum(1 for c in self.comparisons if c.diff_result.status == DiffStatus.NEW)


class VisualRegression:
    """Visual regression test helper.

    This is the object provided by the `visual` fixture.

    Usage:
        def test_page(page, visual):
            page.goto("/checkout")
            visual.snapshot("checkout-empty")

            page.fill("#email", "test@example.com")
            visual.snapshot("checkout-filled")

            # Assert all snapshots match baselines
            visual.assert_match()
    """

    def __init__(
        self,
        test_name: str,
        storage: BaselineStorage,
        threshold: float = 0.1,
        update_mode: bool = False,
        fail_on_missing: bool = False,
        branch: str = "main",
    ):
        self.test_name = test_name
        self.storage = storage
        self.threshold = threshold
        self.update_mode = update_mode
        self.fail_on_missing = fail_on_missing
        self.branch = branch

        self._capture: Optional[VisualCapture] = None
        self._async_capture: Optional[AsyncVisualCapture] = None
        self._snapshots: List[VisualSnapshot] = []
        self._comparisons: List[ComparisonResult] = []
        self._viewport: Optional[str] = None

    def set_page(self, page: PlaywrightPage) -> None:
        """Set Playwright page for capturing.

        Called automatically if using playwright fixture.
        """
        self._capture = VisualCapture.from_playwright(page)

        # Set viewport identifier
        viewport = page.viewport_size
        if viewport:
            self._viewport = f"{viewport['width']}x{viewport['height']}"

    def set_async_page(self, page) -> None:
        """Set async Playwright page."""
        self._async_capture = AsyncVisualCapture(page, device_scale_factor=1.0)

        viewport = page.viewport_size
        if viewport:
            self._viewport = f"{viewport['width']}x{viewport['height']}"

    def snapshot(
        self,
        name: str,
        full_page: bool = False,
        selector: Optional[str] = None,
        threshold: Optional[float] = None,
        ignore_regions: Optional[List[tuple]] = None,
    ) -> ComparisonResult:
        """Take a snapshot and compare to baseline.

        Args:
            name: Unique name for this snapshot
            full_page: Capture full scrollable page
            selector: CSS selector to capture specific element
            threshold: Override default threshold for this snapshot
            ignore_regions: List of (x, y, w, h) regions to ignore

        Returns:
            ComparisonResult
        """
        if self._capture is None:
            raise RuntimeError(
                "No page set. Use visual.set_page(page) or ensure playwright fixture is used."
            )

        # Prefix with test name for uniqueness
        full_name = f"{self.test_name}/{name}"

        # Capture screenshot
        snapshot = self._capture.take(
            name=full_name,
            full_page=full_page,
            element_selector=selector,
        )
        self._snapshots.append(snapshot)

        # Compare to baseline
        comparison = self.storage.compare(
            snapshot=snapshot,
            branch=self.branch,
            viewport=self._viewport,
            threshold=threshold or self.threshold,
        )
        self._comparisons.append(comparison)

        # Handle update mode
        if self.update_mode and comparison.needs_review:
            self.storage.approve(
                snapshot=snapshot,
                branch=self.branch,
                viewport=self._viewport,
                approved_by="pytest --visual-update",
            )
            logger.info(f"Updated baseline: {full_name}")

        # Handle missing baseline
        elif comparison.diff_result.status == DiffStatus.NEW:
            if self.fail_on_missing:
                logger.error(f"Missing baseline: {full_name}")
            else:
                # Auto-create baseline
                self.storage.approve(
                    snapshot=snapshot,
                    branch=self.branch,
                    viewport=self._viewport,
                    approved_by="auto-created",
                )
                logger.info(f"Created new baseline: {full_name}")

        return comparison

    async def snapshot_async(
        self,
        name: str,
        full_page: bool = False,
        selector: Optional[str] = None,
        threshold: Optional[float] = None,
    ) -> ComparisonResult:
        """Async version of snapshot."""
        if self._async_capture is None:
            raise RuntimeError("No async page set. Use visual.set_async_page(page).")

        full_name = f"{self.test_name}/{name}"

        snapshot = await self._async_capture.take(
            name=full_name,
            full_page=full_page,
            element_selector=selector,
        )
        self._snapshots.append(snapshot)

        comparison = self.storage.compare(
            snapshot=snapshot,
            branch=self.branch,
            viewport=self._viewport,
            threshold=threshold or self.threshold,
        )
        self._comparisons.append(comparison)

        if self.update_mode and comparison.needs_review:
            self.storage.approve(
                snapshot=snapshot,
                branch=self.branch,
                viewport=self._viewport,
                approved_by="pytest --visual-update",
            )

        elif comparison.diff_result.status == DiffStatus.NEW and not self.fail_on_missing:
            self.storage.approve(
                snapshot=snapshot,
                branch=self.branch,
                viewport=self._viewport,
                approved_by="auto-created",
            )

        return comparison

    def assert_match(self) -> None:
        """Assert all snapshots match their baselines.

        Raises AssertionError if any snapshot has visual differences.
        """
        failures = []

        for comparison in self._comparisons:
            if comparison.diff_result.status == DiffStatus.DIFF:
                failures.append(
                    f"  - {comparison.snapshot_name}: "
                    f"{comparison.diff_result.diff_percentage:.2f}% different "
                    f"({comparison.diff_result.diff_pixels} pixels)"
                )
            elif comparison.diff_result.status == DiffStatus.SIZE_MISMATCH:
                failures.append(
                    f"  - {comparison.snapshot_name}: "
                    f"Size mismatch (baseline: {comparison.diff_result.baseline_size}, "
                    f"current: {comparison.diff_result.current_size})"
                )
            elif comparison.diff_result.status == DiffStatus.NEW and self.fail_on_missing:
                failures.append(
                    f"  - {comparison.snapshot_name}: Missing baseline"
                )

        if failures:
            failure_msg = "\n".join(failures)
            raise AssertionError(
                f"Visual regression failures:\n{failure_msg}\n\n"
                f"Run with --visual-update to accept these changes."
            )

    @property
    def comparisons(self) -> List[ComparisonResult]:
        """Get all comparison results."""
        return self._comparisons.copy()

    @property
    def snapshots(self) -> List[VisualSnapshot]:
        """Get all captured snapshots."""
        return self._snapshots.copy()


def _get_git_branch() -> str:
    """Get current git branch name."""
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if result.returncode == 0:
            return result.stdout.strip()
    except Exception:
        pass
    return "main"


@pytest.fixture
def visual(request) -> VisualRegression:
    """Pytest fixture for visual regression testing.

    Usage:
        def test_page(page, visual):
            visual.set_page(page)  # Or automatic with playwright
            page.goto("/")
            visual.snapshot("homepage")
            visual.assert_match()
    """
    # Get options
    update_mode = request.config.getoption("--visual-update", False)
    threshold = request.config.getoption("--visual-threshold", 0.1)
    fail_on_missing = request.config.getoption("--visual-fail-on-missing", False)
    baselines_dir = request.config.getoption("--visual-baselines-dir", "baselines")
    storage_type = request.config.getoption("--visual-storage", "auto")

    # Get test name
    test_name = request.node.name

    # Get git branch
    branch = os.environ.get("GITHUB_HEAD_REF") or os.environ.get("CI_COMMIT_BRANCH") or _get_git_branch()

    # Get project ID from environment (for Supabase)
    project_id = os.environ.get("HIKAFLOW_PROJECT_ID")

    # Create storage
    storage = get_storage(
        storage_type=storage_type,
        project_id=project_id,
        base_dir=Path(baselines_dir),
    )

    # Create visual helper
    visual_helper = VisualRegression(
        test_name=test_name,
        storage=storage,
        threshold=threshold,
        update_mode=update_mode,
        fail_on_missing=fail_on_missing,
        branch=branch,
    )

    # Try to auto-detect Playwright page from other fixtures
    if "page" in request.fixturenames:
        try:
            page = request.getfixturevalue("page")
            visual_helper.set_page(page)
        except Exception:
            pass

    yield visual_helper

    # Auto-assert at end of test (unless explicitly called)
    if visual_helper._comparisons and not update_mode:
        # Check if any comparison needs review
        needs_review = any(c.needs_review for c in visual_helper._comparisons)
        if needs_review:
            # Log summary
            diff_count = sum(1 for c in visual_helper._comparisons if c.diff_result.status == DiffStatus.DIFF)
            new_count = sum(1 for c in visual_helper._comparisons if c.diff_result.status == DiffStatus.NEW)

            if diff_count > 0:
                logger.warning(f"Visual regression: {diff_count} snapshots have differences")
            if new_count > 0:
                logger.info(f"Visual regression: {new_count} new baselines created")


# Convenience alias
visual_fixture = visual


def pytest_collection_modifyitems(config, items):
    """Add visual marker to tests using visual fixture."""
    for item in items:
        if "visual" in getattr(item, "fixturenames", []):
            item.add_marker(pytest.mark.visual)


def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """Add visual regression summary to terminal output."""
    visual_results = getattr(config, "_visual_results", [])

    if not visual_results:
        return

    terminalreporter.write_sep("=", "Visual Regression Summary")

    total_snapshots = sum(len(r.comparisons) for r in visual_results)
    total_diffs = sum(r.diff_count for r in visual_results)
    total_new = sum(r.new_count for r in visual_results)

    terminalreporter.write_line(f"Total snapshots: {total_snapshots}")
    terminalreporter.write_line(f"Differences: {total_diffs}")
    terminalreporter.write_line(f"New baselines: {total_new}")

    if total_diffs > 0:
        terminalreporter.write_line(
            "\nRun with --visual-update to accept changes",
            yellow=True,
        )
