from pathlib import Path
from typing import Any

import pytest

from screenshot import (
    HtmlSnapshotRequest,
    MotionSnapshotFormat,
    MotionSnapshotRequest,
    MotionSnapshotResult,
    ScreenshotArtifactsRequest,
    ScreenshotArtifactsResult,
    ScreenshotBatchResult,
    ScreenshotCaptureOutcome,
    ScreenshotCaptureResult,
    ScreenshotOptions,
)
from screenshot._models_options import OPTIONS_SCHEMA_VERSION, CaptureOptions
from screenshot._shared.errors import ScreenshotError, make_error


def test_screenshot_options_merged_viewports() -> None:
    defaults = {
        "desktop": {
            "viewport": {"width": 1280, "height": 720},
            "device_scale_factor": 1.0,
            "is_mobile": False,
            "has_touch": False,
        },
        "mobile": {
            "viewport": {"width": 390, "height": 844},
            "device_scale_factor": 2.5,
            "is_mobile": True,
            "has_touch": True,
        },
    }
    options = ScreenshotOptions(
        capture=CaptureOptions(
            viewports=(
                "desktop",
                {
                    "name": "custom",
                    "viewport": {"width": 800, "height": 600},
                    "device_scale_factor": 1.5,
                    "is_mobile": False,
                },
            )
        )
    )

    merged: dict[str, dict[str, Any]] = options.capture.merged_viewports(defaults)

    assert "desktop" in merged
    assert merged["desktop"]["viewport"]["width"] == 1280
    assert merged["custom"]["viewport"]["height"] == 600


def test_capture_options_rejects_invalid_custom_viewport() -> None:
    with pytest.raises(ValueError, match="non-empty 'name'"):
        CaptureOptions(viewports=({},))


def test_screenshot_capture_result_succeeded_logic(tmp_path: Path) -> None:
    success = ScreenshotCaptureResult(
        requested=1,
        captured=1,
        failed=0,
        metadata_path=tmp_path / "meta.json",
        errors=[],
        job_id="job-1",
    )
    partial = ScreenshotCaptureResult(
        requested=1,
        captured=1,
        failed=1,
        metadata_path=tmp_path / "partial.json",
        errors=[],
        job_id="job-partial",
    )
    failure = ScreenshotCaptureResult(
        requested=1,
        captured=0,
        failed=1,
        metadata_path=None,
        errors=[make_error("runtime", "error")],
        job_id="job-2",
    )
    skipped = ScreenshotCaptureResult(
        requested=0,
        captured=0,
        failed=0,
        metadata_path=None,
        errors=[],
        job_id="job-3",
    )

    assert success.outcome is ScreenshotCaptureOutcome.SUCCESS
    assert partial.outcome is ScreenshotCaptureOutcome.PARTIAL
    assert failure.outcome is ScreenshotCaptureOutcome.FAILED
    assert skipped.outcome is ScreenshotCaptureOutcome.SKIPPED


@pytest.mark.parametrize(
    ("requested", "captured", "failed", "errors", "expected_outcome", "has_artifacts"),
    [
        (1, 1, 0, [], ScreenshotCaptureOutcome.SUCCESS, True),
        (2, 1, 1, [], ScreenshotCaptureOutcome.PARTIAL, True),
        (
            2,
            1,
            0,
            [make_error("runtime", "late failure")],
            ScreenshotCaptureOutcome.PARTIAL,
            True,
        ),
        (
            1,
            0,
            1,
            [make_error("runtime", "hard failure")],
            ScreenshotCaptureOutcome.FAILED,
            False,
        ),
        (
            0,
            0,
            0,
            [make_error("runtime", "zero-request failure")],
            ScreenshotCaptureOutcome.FAILED,
            False,
        ),
        (0, 0, 0, [], ScreenshotCaptureOutcome.SKIPPED, False),
    ],
)
def test_capture_result_outcome_truth_table(
    requested: int,
    captured: int,
    failed: int,
    errors: list[ScreenshotError],
    expected_outcome: ScreenshotCaptureOutcome,
    has_artifacts: bool,
) -> None:
    result = ScreenshotCaptureResult(
        requested=requested,
        captured=captured,
        failed=failed,
        metadata_path=None,
        entries=[],
        errors=errors,
        job_id="truth-table",
    )

    assert result.outcome is expected_outcome
    assert result.has_artifacts is has_artifacts


def test_screenshot_batch_result_serialization(tmp_path: Path) -> None:
    results = [
        ScreenshotCaptureResult(
            requested=1,
            captured=1,
            failed=0,
            metadata_path=tmp_path / "meta.json",
            errors=[],
            job_id="job-1",
        ),
        ScreenshotCaptureResult(
            requested=1,
            captured=1,
            failed=1,
            metadata_path=tmp_path / "partial.json",
            errors=[make_error("timeout", "partial")],
            job_id="job-partial",
        ),
        ScreenshotCaptureResult(
            requested=1,
            captured=0,
            failed=1,
            metadata_path=None,
            errors=[make_error("runtime", "failure")],
            job_id="job-2",
        ),
    ]
    batch = ScreenshotBatchResult(results=results)

    assert len(batch.successes) == 1
    assert len(batch.partials) == 1
    assert len(batch.failures) == 2

    payload: dict[str, Any] = batch.to_dict()
    assert payload["success_count"] == 1
    assert payload["failure_count"] == 2
    assert payload["partial_count"] == 1
    assert payload["skipped_count"] == 0
    assert payload["outcome_counts"] == {
        "success": 1,
        "partial": 1,
        "failed": 1,
        "skipped": 0,
    }
    assert payload["results"][0]["outcome"] == "success"
    assert payload["results"][0]["has_artifacts"] is True
    assert payload["results"][0]["artifacts"] == {
        "html_snapshot_path": None,
        "motion_snapshot": None,
    }
    assert payload["results"][1]["outcome"] == "partial"
    assert payload["results"][2]["errors"][0]["message"] == "failure"
    assert payload["results"][2]["errors"][0]["error_type"] == "runtime"


def test_screenshot_batch_result_from_results_preserves_order_and_counts() -> None:
    skipped = ScreenshotCaptureResult(
        requested=0,
        captured=0,
        failed=0,
        metadata_path=None,
        errors=[],
        job_id="job-skipped",
    )
    partial = ScreenshotCaptureResult(
        requested=1,
        captured=1,
        failed=0,
        metadata_path=None,
        errors=[make_error("runtime", "late error")],
        job_id="job-partial",
    )
    success = ScreenshotCaptureResult(
        requested=1,
        captured=1,
        failed=0,
        metadata_path=None,
        errors=[],
        job_id="job-success",
    )
    failure = ScreenshotCaptureResult(
        requested=1,
        captured=0,
        failed=1,
        metadata_path=None,
        errors=[make_error("runtime", "boom")],
        job_id="job-failure",
    )

    batch = ScreenshotBatchResult.from_results(iter([skipped, partial, success, failure]))

    assert [result.job_id for result in batch.results] == [
        "job-skipped",
        "job-partial",
        "job-success",
        "job-failure",
    ]
    assert [result.job_id for result in batch.successes] == ["job-success"]
    assert [result.job_id for result in batch.partials] == ["job-partial"]
    assert [result.job_id for result in batch.skipped] == ["job-skipped"]
    assert [result.job_id for result in batch.failures] == ["job-partial", "job-failure"]
    assert batch.outcome_counts == {
        "success": 1,
        "partial": 1,
        "failed": 1,
        "skipped": 1,
    }


def test_capture_result_error_messages_property(tmp_path: Path) -> None:
    error = make_error("timeout", "took too long", retryable=True)
    result = ScreenshotCaptureResult(
        requested=1,
        captured=0,
        failed=1,
        metadata_path=None,
        entries=[],
        errors=[error],
        job_id="job-42",
    )

    assert result.error_messages == ["took too long"]


def test_motion_snapshot_request_validates_shape(tmp_path: Path) -> None:
    with pytest.raises(ValueError, match="duration_s must be > 0"):
        MotionSnapshotRequest(path=tmp_path / "clip.gif", duration_s=0.0)

    with pytest.raises(ValueError, match="fps must be >= 1"):
        MotionSnapshotRequest(path=tmp_path / "clip.gif", fps=0)

    with pytest.raises(ValueError, match="path suffix must match format"):
        MotionSnapshotRequest(path=tmp_path / "clip.webp", format=MotionSnapshotFormat.GIF)


def test_artifact_request_and_result_serialization(tmp_path: Path) -> None:
    request = ScreenshotArtifactsRequest(
        html_snapshot=HtmlSnapshotRequest(path=tmp_path / "page.html"),
        motion_snapshot=MotionSnapshotRequest(
            path=tmp_path / "clip.gif",
            duration_s=2.5,
            fps=4,
        ),
    )
    result = ScreenshotArtifactsResult(
        html_snapshot_path=tmp_path / "page.html",
        motion_snapshot=MotionSnapshotResult(
            path=tmp_path / "clip.gif",
            format=MotionSnapshotFormat.GIF,
            duration_s=2.5,
            fps=4,
            written=True,
            truncated=True,
        ),
    )

    assert request.to_dict() == {
        "html_snapshot": {"path": str(tmp_path / "page.html")},
        "motion_snapshot": {
            "path": str(tmp_path / "clip.gif"),
            "format": "gif",
            "duration_s": 2.5,
            "fps": 4,
            "preserve_motion": True,
            "required": False,
        },
    }
    assert result.to_dict() == {
        "html_snapshot_path": str(tmp_path / "page.html"),
        "motion_snapshot": {
            "path": str(tmp_path / "clip.gif"),
            "format": "gif",
            "duration_s": 2.5,
            "fps": 4,
            "written": True,
            "truncated": True,
        },
    }


def test_motion_snapshot_result_requires_path_when_written() -> None:
    with pytest.raises(ValueError, match="must include a path"):
        MotionSnapshotResult(written=True)


def test_screenshot_options_from_dict_validates_schema() -> None:
    with pytest.raises(ValueError, match="Expected schema"):
        ScreenshotOptions.from_dict({"schema_version": "screenshot_options/v1", "capture": {}})


def test_screenshot_options_from_dict_requires_sections() -> None:
    with pytest.raises(ValueError, match="expects at least one"):
        ScreenshotOptions.from_dict({"schema_version": OPTIONS_SCHEMA_VERSION})


def test_screenshot_options_from_dict_accepts_nested_schema() -> None:
    options = ScreenshotOptions.from_dict(
        {
            "schema_version": OPTIONS_SCHEMA_VERSION,
            "capture": {
                "enabled": True,
                "max_pages": 2,
                "viewports": ["desktop", "mobile"],
            },
            "browser": {
                "allow_autoplay": False,
                "hide_overlays": False,
                "compatibility_level": "medium",
            },
            "runner": {
                "extra_styles": ["body { color: green; }"],
                "extra_init_scripts": ["console.log('nested')"],
            },
        }
    )

    assert options.capture.enabled is True
    assert options.capture.max_pages == 2
    assert options.capture.viewports == ("desktop", "mobile")
    assert options.browser.allow_autoplay is False
    assert options.browser.hide_overlays is False
    assert options.browser.compatibility_level == "medium"
    assert options.runner.extra_styles == ("body { color: green; }",)
    assert options.runner.extra_init_scripts == ("console.log('nested')",)


def test_screenshot_options_from_dict_accepts_single_custom_viewport_mapping() -> None:
    options = ScreenshotOptions.from_dict(
        {
            "capture": {
                "enabled": True,
                "viewports": {
                    "name": "marketing",
                    "viewport": {"width": 1600, "height": 900},
                    "device_scale_factor": 1.25,
                },
            }
        }
    )

    assert len(options.capture.viewports) == 1
    custom = options.capture.viewports[0]
    assert isinstance(custom, dict)
    assert custom["name"] == "marketing"
    assert custom["viewport"] == {"width": 1600, "height": 900}
    assert custom["device_scale_factor"] == 1.25
