"""Tests for the native visual regression system."""
import io
import tempfile
from datetime import datetime, timezone
from pathlib import Path

import pytest
from PIL import Image, ImageDraw

from autodebug.visual.capture import (
    VisualCapture,
    VisualSnapshot,
    capture_from_file,
)

# Import our visual regression modules
from autodebug.visual.diff import (
    DiffResult,
    DiffStatus,
    VisualDiff,
    compare_images,
)
from autodebug.visual.storage import (
    ComparisonResult,
    LocalBaselineStorage,
    get_storage,
)

# =============================================================================
# Test Fixtures
# =============================================================================


def create_test_image(
    width: int = 100,
    height: int = 100,
    color: tuple = (255, 255, 255, 255),
    draw_shape: str = None,
    shape_color: tuple = (255, 0, 0, 255),
) -> bytes:
    """Create a test image as PNG bytes."""
    img = Image.new("RGBA", (width, height), color)

    if draw_shape:
        draw = ImageDraw.Draw(img)
        if draw_shape == "circle":
            draw.ellipse([20, 20, 80, 80], fill=shape_color)
        elif draw_shape == "rectangle":
            draw.rectangle([20, 20, 80, 80], fill=shape_color)
        elif draw_shape == "line":
            draw.line([0, 0, width, height], fill=shape_color, width=3)

    buffer = io.BytesIO()
    img.save(buffer, format="PNG")
    return buffer.getvalue()


@pytest.fixture
def identical_images():
    """Two identical images."""
    img = create_test_image(color=(200, 200, 200, 255), draw_shape="circle")
    return img, img


@pytest.fixture
def different_images():
    """Two different images."""
    img1 = create_test_image(color=(200, 200, 200, 255), draw_shape="circle")
    img2 = create_test_image(color=(200, 200, 200, 255), draw_shape="rectangle")
    return img1, img2


@pytest.fixture
def slightly_different_images():
    """Two images with slight color difference."""
    img1 = create_test_image(color=(200, 200, 200, 255))
    img2 = create_test_image(color=(201, 200, 200, 255))  # 1 unit difference
    return img1, img2


@pytest.fixture
def different_size_images():
    """Two images with different sizes."""
    img1 = create_test_image(width=100, height=100)
    img2 = create_test_image(width=150, height=100)
    return img1, img2


@pytest.fixture
def temp_storage_dir():
    """Temporary directory for baseline storage."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir)


# =============================================================================
# Test Image Diffing
# =============================================================================


class TestImageDiffing:
    """Tests for the image comparison algorithm."""

    def test_identical_images_match(self, identical_images):
        """Identical images should have 0% diff."""
        img1, img2 = identical_images
        result = compare_images(img1, img2)

        assert result.status == DiffStatus.MATCH
        assert result.diff_percentage == 0.0
        assert result.diff_pixels == 0
        assert result.passed is True

    def test_different_images_detected(self, different_images):
        """Different images should be detected."""
        img1, img2 = different_images
        result = compare_images(img1, img2)

        assert result.status == DiffStatus.DIFF
        assert result.diff_percentage > 0
        assert result.diff_pixels > 0
        assert result.passed is False
        assert result.has_changes is True

    def test_slight_difference_within_threshold(self, slightly_different_images):
        """Slight differences within threshold should pass."""
        img1, img2 = slightly_different_images
        result = compare_images(img1, img2, threshold=0.5)  # High threshold

        # With high threshold, small color differences should be ignored
        assert result.diff_percentage < 1.0  # Very small difference

    def test_size_mismatch_detected(self, different_size_images):
        """Different sized images should return size_mismatch."""
        img1, img2 = different_size_images
        result = compare_images(img1, img2)

        assert result.status == DiffStatus.SIZE_MISMATCH
        assert result.baseline_size == (100, 100)
        assert result.current_size == (150, 100)

    def test_diff_image_generated(self, different_images):
        """Diff image should be generated for different images."""
        img1, img2 = different_images
        result = compare_images(img1, img2)

        assert result.diff_image is not None
        # Verify it's valid PNG
        img = Image.open(io.BytesIO(result.diff_image))
        assert img.format == "PNG"

    def test_diff_regions_identified(self, different_images):
        """Diff regions should be identified."""
        img1, img2 = different_images
        result = compare_images(img1, img2)

        # Should have at least one diff region
        assert len(result.diff_regions) >= 0  # May be empty for scattered diffs

    def test_custom_threshold(self, different_images):
        """Custom threshold should affect results."""
        img1, img2 = different_images

        strict_result = compare_images(img1, img2, threshold=0.01)
        lenient_result = compare_images(img1, img2, threshold=0.9)

        # Both should detect the difference in this case
        assert strict_result.diff_pixels >= lenient_result.diff_pixels

    def test_ignore_regions(self, different_images):
        """Ignore regions should exclude areas from comparison."""
        img1, img2 = different_images

        # Ignore the center where shapes are drawn
        result_with_ignore = compare_images(
            img1, img2,
            ignore_regions=[(20, 20, 60, 60)]  # Ignore center
        )
        result_without_ignore = compare_images(img1, img2)

        # With ignore, should have fewer diff pixels
        assert result_with_ignore.diff_pixels <= result_without_ignore.diff_pixels

    def test_to_dict_serialization(self, different_images):
        """DiffResult should serialize to dict correctly."""
        img1, img2 = different_images
        result = compare_images(img1, img2)

        data = result.to_dict()

        assert "status" in data
        assert "diff_percentage" in data
        assert "diff_pixels" in data
        assert "total_pixels" in data
        assert "passed" in data
        assert data["status"] == "diff"


class TestVisualDiffClass:
    """Tests for the VisualDiff helper class."""

    def test_visual_diff_compare(self, different_images):
        """VisualDiff class should work correctly."""
        img1, img2 = different_images
        differ = VisualDiff(threshold=0.1)

        result = differ.compare(img1, img2)

        assert isinstance(result, DiffResult)
        assert result.status == DiffStatus.DIFF

    def test_visual_diff_custom_settings(self, different_images):
        """VisualDiff should respect custom settings."""
        img1, img2 = different_images

        differ = VisualDiff(
            threshold=0.5,
            anti_aliasing=False,
            diff_color=(0, 255, 0, 255),
        )

        result = differ.compare(img1, img2)
        assert isinstance(result, DiffResult)

    def test_side_by_side_creation(self, different_images):
        """Side-by-side image should be created."""
        img1, img2 = different_images
        differ = VisualDiff()

        result = differ.compare(img1, img2)
        combined = differ.create_side_by_side(img1, img2, result.diff_image)

        # Should be valid PNG
        img = Image.open(io.BytesIO(combined))
        assert img.format == "PNG"
        # Should be wider than original (3 images side by side)
        assert img.width > 100


# =============================================================================
# Test Visual Capture
# =============================================================================


class TestVisualCapture:
    """Tests for screenshot capture."""

    def test_capture_from_callback(self):
        """Capture should work with custom callback."""
        test_image = create_test_image()

        capture = VisualCapture(
            capture_fn=lambda: test_image,
            url_fn=lambda: "http://test.com",
            viewport_fn=lambda: {"width": 1920, "height": 1080},
        )

        snapshot = capture.take("test-snapshot")

        assert snapshot.name == "test-snapshot"
        assert snapshot.screenshot == test_image
        assert snapshot.url == "http://test.com"
        assert snapshot.viewport_size == {"width": 1920, "height": 1080}

    def test_snapshot_metadata(self):
        """Snapshot should store metadata."""
        test_image = create_test_image()

        capture = VisualCapture(capture_fn=lambda: test_image)
        snapshot = capture.take(
            "test",
            metadata={"custom": "data"},
        )

        assert snapshot.metadata == {"custom": "data"}

    def test_snapshot_content_hash(self):
        """Snapshot should have consistent content hash."""
        test_image = create_test_image()

        capture = VisualCapture(capture_fn=lambda: test_image)
        snapshot1 = capture.take("test1")
        snapshot2 = capture.take("test2")

        # Same image content should have same hash
        assert snapshot1.content_hash == snapshot2.content_hash

    def test_different_content_different_hash(self):
        """Different images should have different hashes."""
        img1 = create_test_image(draw_shape="circle")
        img2 = create_test_image(draw_shape="rectangle")

        snapshot1 = VisualSnapshot(
            name="test1",
            screenshot=img1,
            timestamp=datetime.now(timezone.utc),
        )
        snapshot2 = VisualSnapshot(
            name="test2",
            screenshot=img2,
            timestamp=datetime.now(timezone.utc),
        )

        assert snapshot1.content_hash != snapshot2.content_hash

    def test_snapshots_list(self):
        """Capture should track all snapshots."""
        test_image = create_test_image()
        capture = VisualCapture(capture_fn=lambda: test_image)

        capture.take("snap1")
        capture.take("snap2")
        capture.take("snap3")

        assert len(capture.snapshots) == 3
        assert [s.name for s in capture.snapshots] == ["snap1", "snap2", "snap3"]

    def test_clear_snapshots(self):
        """Clear should remove all snapshots."""
        test_image = create_test_image()
        capture = VisualCapture(capture_fn=lambda: test_image)

        capture.take("snap1")
        capture.take("snap2")
        capture.clear()

        assert len(capture.snapshots) == 0

    def test_snapshot_to_dict(self):
        """Snapshot should serialize to dict."""
        test_image = create_test_image()
        snapshot = VisualSnapshot(
            name="test",
            screenshot=test_image,
            timestamp=datetime.now(timezone.utc),
            url="http://example.com",
            viewport_size={"width": 1920, "height": 1080},
        )

        data = snapshot.to_dict()

        assert data["name"] == "test"
        assert data["url"] == "http://example.com"
        assert "content_hash" in data
        assert "screenshot" not in data  # Should not include bytes


class TestCaptureFromFile:
    """Tests for capturing from existing files."""

    def test_capture_from_file(self, temp_storage_dir):
        """Should create snapshot from file."""
        # Create test image file
        test_image = create_test_image()
        img_path = temp_storage_dir / "test.png"
        img_path.write_bytes(test_image)

        snapshot = capture_from_file(img_path)

        assert snapshot.name == "test"
        assert snapshot.screenshot == test_image
        assert snapshot.metadata["source"] == "file"


# =============================================================================
# Test Baseline Storage
# =============================================================================


class TestLocalBaselineStorage:
    """Tests for local filesystem baseline storage."""

    def test_save_and_get_baseline(self, temp_storage_dir):
        """Should save and retrieve baselines."""
        storage = LocalBaselineStorage(temp_storage_dir)
        test_image = create_test_image()

        # Save baseline
        info = storage.save_baseline(
            name="test-page",
            image=test_image,
            branch="main",
        )

        assert info.name == "test-page"
        assert info.branch == "main"

        # Retrieve baseline
        retrieved = storage.get_baseline("test-page", "main")

        assert retrieved == test_image

    def test_get_nonexistent_baseline(self, temp_storage_dir):
        """Should return None for missing baseline."""
        storage = LocalBaselineStorage(temp_storage_dir)

        result = storage.get_baseline("nonexistent", "main")

        assert result is None

    def test_branch_fallback(self, temp_storage_dir):
        """Should fallback to main branch."""
        storage = LocalBaselineStorage(temp_storage_dir)
        test_image = create_test_image()

        # Save to main
        storage.save_baseline("test", test_image, "main")

        # Request from feature branch - should fallback
        result = storage.get_baseline("test", "feature-branch")

        assert result == test_image

    def test_branch_specific_baseline(self, temp_storage_dir):
        """Should use branch-specific baseline if available."""
        storage = LocalBaselineStorage(temp_storage_dir)
        main_image = create_test_image(draw_shape="circle")
        feature_image = create_test_image(draw_shape="rectangle")

        # Save different baselines per branch
        storage.save_baseline("test", main_image, "main")
        storage.save_baseline("test", feature_image, "feature")

        # Should get branch-specific
        main_result = storage.get_baseline("test", "main")
        feature_result = storage.get_baseline("test", "feature")

        assert main_result == main_image
        assert feature_result == feature_image

    def test_viewport_specific_baseline(self, temp_storage_dir):
        """Should handle viewport-specific baselines."""
        storage = LocalBaselineStorage(temp_storage_dir)
        desktop_image = create_test_image(width=200, height=100)
        mobile_image = create_test_image(width=100, height=200)

        storage.save_baseline("homepage", desktop_image, "main", viewport="1920x1080")
        storage.save_baseline("homepage", mobile_image, "main", viewport="375x667")

        desktop_result = storage.get_baseline("homepage", "main", viewport="1920x1080")
        mobile_result = storage.get_baseline("homepage", "main", viewport="375x667")

        assert desktop_result == desktop_image
        assert mobile_result == mobile_image

    def test_delete_baseline(self, temp_storage_dir):
        """Should delete baselines."""
        storage = LocalBaselineStorage(temp_storage_dir)
        test_image = create_test_image()

        storage.save_baseline("test", test_image, "main")

        # Verify exists
        assert storage.get_baseline("test", "main") is not None

        # Delete
        result = storage.delete_baseline("test", "main")

        assert result is True
        assert storage.get_baseline("test", "main") is None

    def test_list_baselines(self, temp_storage_dir):
        """Should list all baselines."""
        storage = LocalBaselineStorage(temp_storage_dir)
        test_image = create_test_image()

        storage.save_baseline("page1", test_image, "main")
        storage.save_baseline("page2", test_image, "main")
        storage.save_baseline("page3", test_image, "feature")

        all_baselines = storage.list_baselines()
        main_baselines = storage.list_baselines("main")

        assert len(all_baselines) == 3
        assert len(main_baselines) == 2

    def test_compare_with_baseline(self, temp_storage_dir):
        """Storage.compare should work correctly."""
        storage = LocalBaselineStorage(temp_storage_dir)
        baseline_image = create_test_image(draw_shape="circle")
        current_image = create_test_image(draw_shape="rectangle")

        # Save baseline
        storage.save_baseline("test", baseline_image, "main")

        # Create snapshot with different image
        snapshot = VisualSnapshot(
            name="test",
            screenshot=current_image,
            timestamp=datetime.now(timezone.utc),
        )

        # Compare
        result = storage.compare(snapshot, branch="main")

        assert isinstance(result, ComparisonResult)
        assert result.diff_result.status == DiffStatus.DIFF
        assert result.needs_review is True

    def test_compare_new_baseline(self, temp_storage_dir):
        """Compare should return NEW status for missing baseline."""
        storage = LocalBaselineStorage(temp_storage_dir)
        test_image = create_test_image()

        snapshot = VisualSnapshot(
            name="new-page",
            screenshot=test_image,
            timestamp=datetime.now(timezone.utc),
        )

        result = storage.compare(snapshot, branch="main")

        assert result.diff_result.status == DiffStatus.NEW
        assert result.baseline_info is None

    def test_approve_creates_baseline(self, temp_storage_dir):
        """Approve should save snapshot as new baseline."""
        storage = LocalBaselineStorage(temp_storage_dir)
        test_image = create_test_image()

        snapshot = VisualSnapshot(
            name="new-page",
            screenshot=test_image,
            timestamp=datetime.now(timezone.utc),
        )

        info = storage.approve(snapshot, branch="main", approved_by="test-user")

        assert info.name == "new-page"
        assert info.approved_by == "test-user"

        # Should now exist
        retrieved = storage.get_baseline("new-page", "main")
        assert retrieved == test_image


class TestGetStorage:
    """Tests for storage factory function."""

    def test_get_local_storage(self, temp_storage_dir):
        """Should return local storage."""
        storage = get_storage("local", base_dir=temp_storage_dir)

        assert isinstance(storage, LocalBaselineStorage)

    def test_auto_detection_local(self, temp_storage_dir, monkeypatch):
        """Auto should return local when Supabase not configured."""
        monkeypatch.delenv("SUPABASE_URL", raising=False)

        storage = get_storage("auto", base_dir=temp_storage_dir)

        assert isinstance(storage, LocalBaselineStorage)


# =============================================================================
# Integration Tests
# =============================================================================


class TestEndToEndWorkflow:
    """Integration tests for the full workflow."""

    def test_full_comparison_workflow(self, temp_storage_dir):
        """Test complete workflow: capture -> compare -> approve."""
        storage = LocalBaselineStorage(temp_storage_dir)

        # Simulate first run - no baseline exists
        first_image = create_test_image(draw_shape="circle")
        first_snapshot = VisualSnapshot(
            name="checkout-page",
            screenshot=first_image,
            timestamp=datetime.now(timezone.utc),
        )

        # Compare - should be NEW
        first_result = storage.compare(first_snapshot, branch="main")
        assert first_result.diff_result.status == DiffStatus.NEW

        # Approve as baseline
        storage.approve(first_snapshot, branch="main")

        # Simulate second run - same image
        second_snapshot = VisualSnapshot(
            name="checkout-page",
            screenshot=first_image,  # Same image
            timestamp=datetime.now(timezone.utc),
        )

        # Compare - should MATCH
        second_result = storage.compare(second_snapshot, branch="main")
        assert second_result.diff_result.status == DiffStatus.MATCH
        assert second_result.passed is True

        # Simulate third run - different image
        changed_image = create_test_image(draw_shape="rectangle")
        third_snapshot = VisualSnapshot(
            name="checkout-page",
            screenshot=changed_image,
            timestamp=datetime.now(timezone.utc),
        )

        # Compare - should DIFF
        third_result = storage.compare(third_snapshot, branch="main")
        assert third_result.diff_result.status == DiffStatus.DIFF
        assert third_result.needs_review is True

    def test_branch_workflow(self, temp_storage_dir):
        """Test branch-specific baseline workflow."""
        storage = LocalBaselineStorage(temp_storage_dir)

        main_image = create_test_image(color=(255, 255, 255, 255))
        feature_image = create_test_image(color=(200, 200, 200, 255))

        # Set up main baseline
        main_snapshot = VisualSnapshot(
            name="header",
            screenshot=main_image,
            timestamp=datetime.now(timezone.utc),
        )
        storage.approve(main_snapshot, branch="main")

        # On feature branch, image is different
        feature_snapshot = VisualSnapshot(
            name="header",
            screenshot=feature_image,
            timestamp=datetime.now(timezone.utc),
        )

        # Compare on feature branch - should diff against main's baseline
        result = storage.compare(feature_snapshot, branch="feature-dark-mode")
        assert result.diff_result.status == DiffStatus.DIFF

        # Approve for feature branch
        storage.approve(feature_snapshot, branch="feature-dark-mode")

        # Now compare on feature branch - should match feature baseline
        result2 = storage.compare(feature_snapshot, branch="feature-dark-mode")
        assert result2.diff_result.status == DiffStatus.MATCH


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
