# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""Tests for Gallery Vault — watermark, provenance, skill, chain anchor."""

import json
import os
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from PIL import Image


def _make_test_image(path, width=100, height=100, color=(128, 64, 192)):
    """Create a small test PNG image."""
    img = Image.new("RGB", (width, height), color)
    img.save(path, "PNG")
    return path


# ── Watermark Tests ───────────────────────────────────────────


class TestWatermark:
    """Tests for watermark.py image processing functions."""

    def test_compute_image_hash(self, tmp_path):
        """SHA-256 hash of image file bytes."""
        from familiar.skills.gallery.watermark import compute_image_hash

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path)

        h = compute_image_hash(img_path)
        assert len(h) == 64  # SHA-256 hex
        assert h == compute_image_hash(img_path)  # deterministic

    def test_compute_hash_different_images(self, tmp_path):
        """Different images produce different hashes."""
        from familiar.skills.gallery.watermark import compute_image_hash

        img1 = str(tmp_path / "a.png")
        img2 = str(tmp_path / "b.png")
        _make_test_image(img1, color=(255, 0, 0))
        _make_test_image(img2, color=(0, 255, 0))

        assert compute_image_hash(img1) != compute_image_hash(img2)

    def test_visible_watermark_creates_file(self, tmp_path):
        """Visible watermark produces an output file."""
        from familiar.skills.gallery.watermark import apply_visible_watermark

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path, 200, 200)

        out = apply_visible_watermark(img_path, "Test Watermark")
        assert os.path.exists(out)
        assert "_watermarked" in out

    def test_visible_watermark_positions(self, tmp_path):
        """All 4 positions produce valid output."""
        from familiar.skills.gallery.watermark import apply_visible_watermark

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path, 200, 200)

        for pos in ("bottom-right", "bottom-left", "center", "tiled"):
            out = str(tmp_path / f"wm_{pos}.png")
            result = apply_visible_watermark(img_path, "WM", position=pos, output_path=out)
            assert os.path.exists(result)

    def test_visible_watermark_custom_opacity(self, tmp_path):
        """Custom opacity is accepted."""
        from familiar.skills.gallery.watermark import apply_visible_watermark

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path, 200, 200)

        out = str(tmp_path / "wm_opacity.png")
        result = apply_visible_watermark(img_path, "WM", opacity=0.8, output_path=out)
        assert os.path.exists(result)

    def test_invisible_watermark_roundtrip(self, tmp_path):
        """Data embedded via LSB can be extracted."""
        from familiar.skills.gallery.watermark import (
            apply_invisible_watermark,
            extract_invisible_watermark,
        )

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path, 200, 200)

        secret = "Hello, this is a secret message!"
        out = apply_invisible_watermark(img_path, secret)
        extracted = extract_invisible_watermark(out)
        assert extracted == secret

    def test_invisible_watermark_json_roundtrip(self, tmp_path):
        """JSON data survives LSB embed/extract cycle."""
        from familiar.skills.gallery.watermark import (
            apply_invisible_watermark,
            extract_invisible_watermark,
        )

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path, 200, 200)

        data = json.dumps({"artist": "Test", "id": "abc123"})
        out = apply_invisible_watermark(img_path, data)
        extracted = extract_invisible_watermark(out)
        assert json.loads(extracted) == json.loads(data)

    def test_invisible_watermark_capacity_overflow(self, tmp_path):
        """Embedding too much data raises ValueError."""
        from familiar.skills.gallery.watermark import apply_invisible_watermark

        img_path = str(tmp_path / "tiny.png")
        _make_test_image(img_path, 2, 2)  # 2x2 = 12 bits capacity

        with pytest.raises(ValueError, match="too large"):
            apply_invisible_watermark(img_path, "A" * 100)

    def test_invisible_watermark_forces_png(self, tmp_path):
        """LSB output is always PNG regardless of input."""
        from familiar.skills.gallery.watermark import apply_invisible_watermark

        img_path = str(tmp_path / "test.jpg")
        img = Image.new("RGB", (100, 100), (128, 64, 192))
        img.save(img_path, "JPEG")

        out = apply_invisible_watermark(img_path, "test")
        assert out.endswith(".png")

    def test_generate_thumbnail(self, tmp_path):
        """Thumbnail is created at correct max size."""
        from familiar.skills.gallery.watermark import generate_thumbnail

        img_path = str(tmp_path / "big.png")
        _make_test_image(img_path, 800, 600)

        out = generate_thumbnail(img_path, size=(200, 200))
        thumb = Image.open(out)
        assert thumb.width <= 200
        assert thumb.height <= 200

    def test_generate_thumbnail_preserves_aspect(self, tmp_path):
        """Thumbnail preserves original aspect ratio."""
        from familiar.skills.gallery.watermark import generate_thumbnail

        img_path = str(tmp_path / "wide.png")
        _make_test_image(img_path, 400, 200)

        out = generate_thumbnail(img_path, size=(100, 100))
        thumb = Image.open(out)
        assert thumb.width == 100
        assert thumb.height == 50


# ── Provenance Tests ──────────────────────────────────────────


class TestProvenance:
    """Tests for provenance.py certificate and verification."""

    def test_generate_certificate_markdown(self):
        """Certificate contains expected markdown fields."""
        from familiar.skills.gallery.provenance import generate_certificate

        artwork = {
            "title": "Sunset Harbor",
            "artist": "Jane Doe",
            "artwork_id": "abc123",
            "registered_at": "2026-03-07T12:00:00Z",
            "original_hash": "a" * 64,
            "watermarked_hash": "b" * 64,
            "block_hash": "c" * 64,
            "block_index": 5,
            "genesis_hash": "d" * 64,
            "watermark_types": ["visible", "invisible"],
            "medium": "Oil on Canvas",
            "dimensions": "24x36 inches",
            "license": "CC BY 4.0",
            "tags": ["landscape", "sunset"],
            "description": "A harbor at dusk.",
        }

        cert = generate_certificate(artwork)
        assert "# Provenance Certificate" in cert
        assert "Sunset Harbor" in cert
        assert "Jane Doe" in cert
        assert "a" * 64 in cert
        assert "Block Hash" in cert
        assert "landscape, sunset" in cert

    def test_generate_certificate_no_chain(self):
        """Certificate handles missing chain data gracefully."""
        from familiar.skills.gallery.provenance import generate_certificate

        artwork = {
            "title": "Test",
            "artist": "Test Artist",
            "artwork_id": "xyz",
            "registered_at": "2026-01-01",
            "original_hash": "0" * 64,
            "watermarked_hash": "1" * 64,
            "block_hash": "",
            "watermark_types": [],
            "medium": "",
            "dimensions": "",
            "license": "All Rights Reserved",
            "tags": [],
        }

        cert = generate_certificate(artwork)
        assert "notarization pending" in cert

    @patch("familiar.skills.gallery.provenance._load_gallery_data")
    def test_verify_artwork_not_found(self, mock_load):
        """Verification returns not-found for unknown artwork."""
        from familiar.skills.gallery.provenance import verify_artwork

        mock_load.return_value = []
        result = verify_artwork("nonexistent")
        assert result["verified"] is False
        assert "not found" in result["details"]

    @patch("familiar.skills.gallery.provenance._load_gallery_data")
    def test_verify_artwork_hash_match(self, mock_load, tmp_path):
        """Verification detects matching hash."""
        from familiar.skills.gallery.provenance import verify_artwork
        from familiar.skills.gallery.watermark import compute_image_hash

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path)
        real_hash = compute_image_hash(img_path)

        mock_load.return_value = [{
            "artwork_id": "test1",
            "watermarked_path": img_path,
            "watermarked_hash": real_hash,
        }]

        with patch("familiar.skills.gallery.provenance.get_chain_blocks_for_artwork", return_value=[]):
            with patch("familiar.skills.training.bridge.get_chain_status",
                       return_value={"initialized": False}):
                result = verify_artwork("test1")
                assert result["hash_match"] is True

    @patch("familiar.skills.gallery.provenance._load_gallery_data")
    def test_verify_artwork_hash_mismatch(self, mock_load, tmp_path):
        """Verification detects tampered file."""
        from familiar.skills.gallery.provenance import verify_artwork

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path)

        mock_load.return_value = [{
            "artwork_id": "test2",
            "watermarked_path": img_path,
            "watermarked_hash": "0" * 64,  # wrong hash
        }]

        with patch("familiar.skills.gallery.provenance.get_chain_blocks_for_artwork", return_value=[]):
            with patch("familiar.skills.training.bridge.get_chain_status",
                       return_value={"initialized": False}):
                result = verify_artwork("test2")
                assert result["hash_match"] is False
                assert "mismatch" in result["details"].lower() or "altered" in result["details"].lower()

    @patch("familiar.skills.gallery.provenance._get_gallery_dir")
    @patch("familiar.skills.gallery.provenance._load_gallery_data")
    @patch("familiar.skills.gallery.provenance._save_gallery_data")
    @patch("familiar.skills.training.bridge.get_chain_status",
           return_value={"initialized": True, "genesis_hash": "g" * 64})
    @patch("familiar.skills.training.bridge.anchor_watermark", return_value="block" + "0" * 60)
    def test_register_artwork_full_pipeline(
        self, mock_anchor, mock_status, mock_save, mock_load, mock_dir, tmp_path
    ):
        """Full registration pipeline creates artwork record."""
        from familiar.skills.gallery.provenance import register_artwork

        mock_dir.return_value = tmp_path
        for sub in ("originals", "watermarked", "thumbnails", "certificates"):
            (tmp_path / sub).mkdir()
        mock_load.return_value = []

        img_path = str(tmp_path / "source.png")
        _make_test_image(img_path, 200, 200)

        artwork = register_artwork(
            image_path=img_path,
            title="Test Piece",
            artist="Test Artist",
            medium="Digital",
            tags=["test"],
        )

        assert artwork["title"] == "Test Piece"
        assert artwork["artist"] == "Test Artist"
        assert len(artwork["artwork_id"]) == 12
        assert artwork["original_hash"]
        assert artwork["watermarked_hash"]
        assert "visible" in artwork["watermark_types"]
        assert "invisible" in artwork["watermark_types"]
        assert artwork["certificate_path"]
        assert Path(artwork["certificate_path"]).exists()
        mock_save.assert_called_once()

    @patch("familiar.skills.gallery.provenance._get_gallery_dir")
    @patch("familiar.skills.gallery.provenance._load_gallery_data")
    @patch("familiar.skills.gallery.provenance._save_gallery_data")
    @patch("familiar.skills.training.bridge.get_chain_status",
           return_value={"initialized": False})
    @patch("familiar.skills.training.bridge.anchor_watermark", return_value="")
    def test_register_artwork_no_chain(
        self, mock_anchor, mock_status, mock_save, mock_load, mock_dir, tmp_path
    ):
        """Registration works without chain (no genesis block)."""
        from familiar.skills.gallery.provenance import register_artwork

        mock_dir.return_value = tmp_path
        for sub in ("originals", "watermarked", "thumbnails", "certificates"):
            (tmp_path / sub).mkdir()
        mock_load.return_value = []

        img_path = str(tmp_path / "source.png")
        _make_test_image(img_path, 200, 200)

        artwork = register_artwork(
            image_path=img_path,
            title="No Chain Art",
            artist="Artist",
            watermark_types=["visible", "certificate"],
        )

        assert artwork["block_hash"] == ""
        assert artwork["certificate_path"]
        cert_text = Path(artwork["certificate_path"]).read_text()
        assert "notarization pending" in cert_text


# ── Gallery Skill Tests ───────────────────────────────────────


class TestGallerySkill:
    """Tests for skill.py tool handlers and TOOLS list."""

    def test_tools_list_structure(self):
        """TOOLS list has correct structure."""
        from familiar.skills.gallery.skill import TOOLS

        assert len(TOOLS) == 5
        names = [t["name"] for t in TOOLS]
        assert "watermark_art" in names
        assert "register_artwork" in names
        assert "verify_artwork" in names
        assert "gallery_list" in names
        assert "gallery_search" in names

        for tool in TOOLS:
            assert "name" in tool
            assert "description" in tool
            assert "input_schema" in tool
            assert "handler" in tool
            assert tool["category"] == "gallery"
            assert callable(tool["handler"])

    def test_watermark_art_missing_file(self):
        """watermark_art returns error for missing file."""
        from familiar.skills.gallery.skill import watermark_art

        result = watermark_art({"image_path": "/nonexistent/image.png"})
        assert result.get("success") is False or "error" in result

    def test_watermark_art_visible(self, tmp_path):
        """watermark_art applies visible watermark."""
        from familiar.skills.gallery.skill import watermark_art

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path, 200, 200)

        result = watermark_art({
            "image_path": img_path,
            "text": "Copyright",
            "type": "visible",
        })
        assert result["success"] is True
        assert os.path.exists(result["output_path"])

    def test_watermark_art_invisible(self, tmp_path):
        """watermark_art applies invisible watermark."""
        from familiar.skills.gallery.skill import watermark_art

        img_path = str(tmp_path / "test.png")
        _make_test_image(img_path, 200, 200)

        result = watermark_art({
            "image_path": img_path,
            "type": "invisible",
            "data": "secret data",
        })
        assert result["success"] is True
        assert result["type"] == "invisible"

    def test_gallery_list_empty(self, tmp_path):
        """gallery_list returns empty for fresh gallery."""
        from familiar.skills.gallery.skill import gallery_list

        with patch("familiar.skills.gallery.skill._load_gallery_data", return_value=[]):
            result = gallery_list({})
            assert result["success"] is True
            assert result["count"] == 0

    def test_gallery_list_with_data(self):
        """gallery_list returns artwork summaries."""
        from familiar.skills.gallery.skill import gallery_list

        mock_data = [
            {
                "artwork_id": "abc",
                "title": "Test",
                "artist": "Artist",
                "registered_at": "2026-01-01",
                "watermark_types": ["visible"],
                "block_hash": "x" * 64,
                "thumbnail_path": "/tmp/thumb.png",
                "tags": ["test"],
            }
        ]

        with patch("familiar.skills.gallery.skill._load_gallery_data", return_value=mock_data):
            result = gallery_list({})
            assert result["count"] == 1
            assert result["artworks"][0]["artwork_id"] == "abc"

    def test_gallery_search_matches(self):
        """gallery_search filters by query."""
        from familiar.skills.gallery.skill import gallery_search

        mock_data = [
            {"artwork_id": "a", "title": "Sunset Harbor", "artist": "Jane",
             "description": "", "medium": "Oil", "tags": ["landscape"],
             "registered_at": "", "watermark_types": [], "block_hash": ""},
            {"artwork_id": "b", "title": "Mountain Peak", "artist": "Bob",
             "description": "", "medium": "Digital", "tags": ["mountain"],
             "registered_at": "", "watermark_types": [], "block_hash": ""},
        ]

        with patch("familiar.skills.gallery.skill._load_gallery_data", return_value=mock_data):
            result = gallery_search({"query": "sunset"})
            assert result["count"] == 1
            assert result["artworks"][0]["artwork_id"] == "a"

    def test_gallery_search_by_tag(self):
        """gallery_search matches tags."""
        from familiar.skills.gallery.skill import gallery_search

        mock_data = [
            {"artwork_id": "a", "title": "Test", "artist": "Artist",
             "description": "", "medium": "", "tags": ["portrait", "oil"],
             "registered_at": "", "watermark_types": [], "block_hash": ""},
        ]

        with patch("familiar.skills.gallery.skill._load_gallery_data", return_value=mock_data):
            result = gallery_search({"query": "portrait"})
            assert result["count"] == 1

    def test_verify_artwork_tool_missing_id(self):
        """verify_artwork_tool requires artwork_id."""
        from familiar.skills.gallery.skill import verify_artwork_tool

        result = verify_artwork_tool({})
        assert "error" in result


# ── Chain Anchor Tests ────────────────────────────────────────


class TestChainAnchor:
    """Tests for anchor_watermark in bridge.py."""

    @patch("familiar.skills.training.bridge._get_keypair")
    @patch("familiar.skills.training.bridge._get_store")
    def test_anchor_watermark_no_genesis(self, mock_store, mock_keys):
        """anchor_watermark returns empty string without genesis."""
        from familiar.skills.training.bridge import anchor_watermark

        store = MagicMock()
        store.get_genesis.return_value = None
        mock_store.return_value = store

        result = anchor_watermark({"artwork_id": "test"})
        assert result == ""

    @patch("familiar.skills.training.bridge._get_keypair")
    @patch("familiar.skills.training.bridge._get_store")
    def test_anchor_watermark_with_genesis(self, mock_store, mock_keys):
        """anchor_watermark creates a watermark block."""
        from familiar.skills.training.bridge import anchor_watermark

        mock_genesis = MagicMock()
        mock_genesis.block_hash = "genesis_hash"
        mock_latest = MagicMock()
        mock_latest.index = 3
        mock_latest.block_hash = "prev_hash_value"

        store = MagicMock()
        store.get_genesis.return_value = mock_genesis
        store.get_latest.return_value = mock_latest
        mock_store.return_value = store

        mock_private = MagicMock()
        mock_keys.return_value = (mock_private, MagicMock())

        # Mock ChainBlock to capture what's created
        with patch("familiar.core.genesis.ChainBlock") as MockBlock:
            mock_block = MagicMock()
            mock_block.block_hash = "watermark_block_hash"
            MockBlock.return_value = mock_block

            payload = {
                "artwork_id": "art123",
                "title": "Test",
                "artist": "Artist",
                "original_hash": "o" * 64,
                "watermarked_hash": "w" * 64,
                "watermark_types": ["visible"],
                "license": "All Rights Reserved",
            }

            result = anchor_watermark(payload)
            assert result == "watermark_block_hash"

            # Verify block was constructed correctly
            call_kwargs = MockBlock.call_args[1]
            assert call_kwargs["block_type"] == "watermark"
            assert call_kwargs["index"] == 4
            assert call_kwargs["prev_hash"] == "prev_hash_value"
            assert call_kwargs["payload"] == payload

            mock_block.compute_hash.assert_called_once()
            mock_block.sign.assert_called_once_with(mock_private)
            store.append.assert_called_once_with(mock_block)

    @patch("familiar.skills.training.bridge._get_keypair")
    @patch("familiar.skills.training.bridge._get_store")
    def test_anchor_watermark_payload_structure(self, mock_store, mock_keys):
        """anchor_watermark passes through full payload."""
        from familiar.skills.training.bridge import anchor_watermark

        mock_genesis = MagicMock()
        mock_latest = MagicMock()
        mock_latest.index = 0
        mock_latest.block_hash = "prev"

        store = MagicMock()
        store.get_genesis.return_value = mock_genesis
        store.get_latest.return_value = mock_latest
        mock_store.return_value = store
        mock_keys.return_value = (MagicMock(), MagicMock())

        with patch("familiar.core.genesis.ChainBlock") as MockBlock:
            mock_block = MagicMock()
            mock_block.block_hash = "hash"
            MockBlock.return_value = mock_block

            full_payload = {
                "artwork_id": "x",
                "title": "T",
                "artist": "A",
                "original_hash": "oh",
                "watermarked_hash": "wh",
                "watermark_types": ["visible", "invisible", "certificate"],
                "license": "CC BY 4.0",
                "certificate_hash": "ch",
                "metadata": {
                    "medium": "Digital",
                    "dimensions": "1920x1080",
                    "file_size": 12345,
                    "format": "PNG",
                },
            }

            anchor_watermark(full_payload)
            assert MockBlock.call_args[1]["payload"] == full_payload

    @patch("familiar.skills.training.bridge._get_keypair")
    @patch("familiar.skills.training.bridge._get_store")
    def test_anchor_watermark_block_type(self, mock_store, mock_keys):
        """anchor_watermark uses block_type='watermark'."""
        from familiar.skills.training.bridge import anchor_watermark

        mock_genesis = MagicMock()
        mock_latest = MagicMock()
        mock_latest.index = 1
        mock_latest.block_hash = "p"
        store = MagicMock()
        store.get_genesis.return_value = mock_genesis
        store.get_latest.return_value = mock_latest
        mock_store.return_value = store
        mock_keys.return_value = (MagicMock(), MagicMock())

        with patch("familiar.core.genesis.ChainBlock") as MockBlock:
            mock_block = MagicMock()
            mock_block.block_hash = "h"
            MockBlock.return_value = mock_block

            anchor_watermark({"artwork_id": "test"})
            assert MockBlock.call_args[1]["block_type"] == "watermark"
