"""Tests for the Content Handle system."""

import pytest

from mcard.model.card import MCard
from mcard.model.card_collection import CardCollection
from mcard.model.handle import ContentHandle, HandleValidationError, validate_handle


class TestHandleValidation:
    """Tests for handle string validation."""

    def test_valid_handles(self):
        """Valid handles should pass validation."""
        assert validate_handle("my_document") == "my_document"
        assert validate_handle("project_v2") == "project_v2"
        assert validate_handle("a") == "a"
        assert validate_handle("abc123") == "abc123"
        assert validate_handle("meeting_notes_2024") == "meeting_notes_2024"

    def test_handles_are_normalized_to_lowercase(self):
        """Handles with uppercase should be normalized to lowercase."""
        assert validate_handle("MyDocument") == "mydocument"
        assert validate_handle("PROJECT_V2") == "project_v2"
        assert validate_handle("MixedCase123") == "mixedcase123"

    def test_invalid_empty_handle(self):
        """Empty handles should raise an error."""
        with pytest.raises(HandleValidationError):
            validate_handle("")
        with pytest.raises(HandleValidationError):
            validate_handle("   ")

    def test_invalid_handle_starts_with_digit(self):
        """Handles that start with a digit should raise an error."""
        with pytest.raises(HandleValidationError):
            validate_handle("1document")
        with pytest.raises(HandleValidationError):
            validate_handle("123abc")

    def test_valid_handle_with_hyphen(self):
        """Handles with hyphens should be valid (allowed for internationalization)."""
        assert validate_handle("my-document") == "my-document"
        assert validate_handle("kebab-case-name") == "kebab-case-name"

    def test_valid_handle_with_period(self):
        """Handles with periods should be valid (for file extensions)."""
        assert validate_handle("my.document") == "my.document"
        assert validate_handle("file.txt") == "file.txt"

    def test_valid_handle_with_special_chars(self):
        """Handles with forward slashes and spaces should be valid (for file paths)."""
        assert validate_handle("path/to/doc") == "path/to/doc"
        assert validate_handle("my document") == "my document"
        assert validate_handle("doc:name") == "doc:name"
        # But backslash and question mark should still fail
        with pytest.raises(HandleValidationError):
            validate_handle("path\\to\\doc")
        with pytest.raises(HandleValidationError):
            validate_handle("doc?query")

    def test_valid_handle_with_spaces(self):
        """Handles with spaces should be valid (for filenames with spaces)."""
        assert validate_handle("my document") == "my document"

    def test_handle_max_length(self):
        """Handles at max length (255) should be valid, longer should fail."""
        # 255 characters
        max_handle = "a" + "b" * 254
        assert len(max_handle) == 255
        assert validate_handle(max_handle) == max_handle

        # 256 characters should fail
        too_long = "a" + "b" * 255
        assert len(too_long) == 256
        with pytest.raises(HandleValidationError):
            validate_handle(too_long)


class TestContentHandleClass:
    """Tests for the ContentHandle class."""

    def test_content_handle_creation(self):
        """ContentHandle should be created with valid inputs."""
        handle = ContentHandle("my_document", "abc123def456")
        assert handle.handle == "my_document"
        assert handle.current_hash == "abc123def456"
        assert handle.created_at is not None
        assert handle.updated_at is not None

    def test_content_handle_update(self):
        """ContentHandle.update() should update the hash and return the previous one."""
        handle = ContentHandle("my_document", "old_hash_value")
        previous = handle.update("new_hash_value")

        assert previous == "old_hash_value"
        assert handle.current_hash == "new_hash_value"

    def test_content_handle_to_dict(self):
        """ContentHandle.to_dict() should return a dictionary representation."""
        handle = ContentHandle("my_document", "abc123def456")
        d = handle.to_dict()

        assert d["handle"] == "my_document"
        assert d["current_hash"] == "abc123def456"
        assert "created_at" in d
        assert "updated_at" in d


class TestHandleOperations:
    """Tests for handle operations in CardCollection."""

    def test_add_with_handle_and_resolve(self):
        """Adding a card with a handle should allow resolving by handle."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("Hello, World!")
        hash_val = collection.add_with_handle(card, "hello_world")

        resolved = collection.get_by_handle("hello_world")
        assert resolved is not None
        assert resolved.hash == hash_val
        assert resolved.get_content(as_text=True) == "Hello, World!"

    def test_resolve_handle_returns_hash(self):
        """resolve_handle should return the current hash."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("Test Content")
        hash_val = collection.add_with_handle(card, "test_doc")

        resolved_hash = collection.resolve_handle("test_doc")
        assert resolved_hash == hash_val

    def test_resolve_nonexistent_handle_returns_none(self):
        """Resolving a non-existent handle should return None."""
        collection = CardCollection(db_path=":memory:")
        assert collection.resolve_handle("nonexistent") is None
        assert collection.get_by_handle("nonexistent") is None

    def test_update_handle_points_to_new_content(self):
        """Updating a handle should point it to the new content."""
        collection = CardCollection(db_path=":memory:")

        # Add initial version
        card1 = MCard("Version 1")
        collection.add_with_handle(card1, "my_doc")

        # Update to new version
        card2 = MCard("Version 2")
        new_hash = collection.update_handle("my_doc", card2)

        # Resolve should return new content
        resolved = collection.get_by_handle("my_doc")
        assert resolved is not None
        assert resolved.hash == new_hash
        assert resolved.get_content(as_text=True) == "Version 2"

    def test_update_handle_preserves_history(self):
        """Updating a handle should record the previous hash in history."""
        collection = CardCollection(db_path=":memory:")

        # Add initial version
        card1 = MCard("Version 1")
        hash1 = collection.add_with_handle(card1, "my_doc")

        # Update to version 2
        card2 = MCard("Version 2")
        collection.update_handle("my_doc", card2)

        # Check history
        history = collection.get_handle_history("my_doc")
        assert len(history) == 1
        assert history[0]["previous_hash"] == hash1

    def test_multiple_updates_build_history(self):
        """Multiple updates should build a complete history."""
        collection = CardCollection(db_path=":memory:")

        card1 = MCard("V1")
        hash1 = collection.add_with_handle(card1, "doc")

        card2 = MCard("V2")
        hash2 = collection.update_handle("doc", card2)

        card3 = MCard("V3")
        collection.update_handle("doc", card3)

        history = collection.get_handle_history("doc")
        assert len(history) == 2
        # History is ordered newest first
        assert history[0]["previous_hash"] == hash2
        assert history[1]["previous_hash"] == hash1

    def test_duplicate_handle_registration_raises_error(self):
        """Registering the same handle twice should raise an error."""
        collection = CardCollection(db_path=":memory:")

        card1 = MCard("Content 1")
        collection.add_with_handle(card1, "unique_handle")

        card2 = MCard("Content 2")
        with pytest.raises(ValueError, match="already exists"):
            collection.add_with_handle(card2, "unique_handle")

    def test_update_nonexistent_handle_raises_error(self):
        """Updating a non-existent handle should raise an error."""
        collection = CardCollection(db_path=":memory:")

        card = MCard("Some content")
        with pytest.raises(ValueError, match="not found"):
            collection.update_handle("nonexistent_handle", card)

    def test_handle_case_insensitivity(self):
        """Handles should be case-insensitive (normalized to lowercase)."""
        collection = CardCollection(db_path=":memory:")

        card = MCard("Test Content")
        collection.add_with_handle(card, "MyDocument")

        # Lookup with different cases should work
        resolved = collection.get_by_handle("mydocument")
        assert resolved is not None

        resolved = collection.get_by_handle("MYDOCUMENT")
        assert resolved is not None


class TestUTF8Handles:
    """Tests for UTF-8/Unicode handle support."""

    def test_chinese_handle(self):
        """Chinese characters should be valid handles."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("中文內容")
        hash_val = collection.add_with_handle(card, "文檔")

        resolved = collection.get_by_handle("文檔")
        assert resolved is not None
        assert resolved.hash == hash_val

    def test_arabic_handle(self):
        """Arabic characters should be valid handles."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("محتوى عربي")
        hash_val = collection.add_with_handle(card, "مستند")

        resolved = collection.get_by_handle("مستند")
        assert resolved is not None
        assert resolved.hash == hash_val

    def test_japanese_handle(self):
        """Japanese characters should be valid handles."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("日本語コンテンツ")
        hash_val = collection.add_with_handle(card, "ドキュメント")

        resolved = collection.get_by_handle("ドキュメント")
        assert resolved is not None
        assert resolved.hash == hash_val

    def test_russian_handle(self):
        """Russian Cyrillic characters should be valid handles."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("Русское содержимое")
        hash_val = collection.add_with_handle(card, "документ")

        resolved = collection.get_by_handle("документ")
        assert resolved is not None
        assert resolved.hash == hash_val

    def test_mixed_script_handle(self):
        """Mixed scripts (e.g., latin + numbers) should work."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("Mixed content")
        hash_val = collection.add_with_handle(card, "doc文檔123")

        resolved = collection.get_by_handle("doc文檔123")
        assert resolved is not None
        assert resolved.hash == hash_val

    def test_unicode_normalization(self):
        """Handles should be NFC normalized for consistent lookup."""
        # é as single character (U+00E9) and composed (e + U+0301) should match
        collection = CardCollection(db_path=":memory:")
        card = MCard("Content")
        collection.add_with_handle(card, "café")  # Using pre-composed é

        # Should resolve with the same handle
        resolved = collection.get_by_handle("café")
        assert resolved is not None


class TestMonadicMethods:
    """Tests for monadic handle retrieval methods."""

    def test_get_by_handle_m_just(self):
        """get_by_handle_m returns Just(card) when found."""

        collection = CardCollection(db_path=":memory:")
        card = MCard("Test Content")
        collection.add_with_handle(card, "mydocument")

        result = collection.get_by_handle_m("mydocument")
        assert result.is_just
        assert result.value.get_content(as_text=True) == "Test Content"

    def test_get_by_handle_m_nothing(self):
        """get_by_handle_m returns Nothing when not found."""
        collection = CardCollection(db_path=":memory:")

        result = collection.get_by_handle_m("nonexistent")
        assert result.is_nothing

    def test_resolve_handle_m_just(self):
        """resolve_handle_m returns Just(hash) when found."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("Test Content")
        expected_hash = collection.add_with_handle(card, "mydoc")

        result = collection.resolve_handle_m("mydoc")
        assert result.is_just
        assert result.value == expected_hash

    def test_maybe_bind_chaining(self):
        """Maybe monad enables chained operations with bind."""
        from mcard.ptr.core.clm_template import Maybe

        collection = CardCollection(db_path=":memory:")
        card = MCard("Hello, World!")
        collection.add_with_handle(card, "greeting")

        # Chain: resolve handle -> get card -> extract content
        result = (
            collection.resolve_handle_m("greeting")
            .bind(lambda h: collection.get_m(h))
            .bind(lambda c: Maybe.just(c.get_content(as_text=True)))
        )

        assert result.is_just
        assert result.value == "Hello, World!"

    def test_maybe_bind_short_circuits_on_nothing(self):
        """Maybe.bind short-circuits when encountering Nothing."""
        from mcard.ptr.core.clm_template import Maybe

        collection = CardCollection(db_path=":memory:")

        # Chain should short-circuit at first Nothing
        result = (
            collection.resolve_handle_m("nonexistent")
            .bind(lambda h: collection.get_m(h))
            .bind(lambda c: Maybe.just(c.get_content(as_text=True)))
        )

        assert result.is_nothing

    def test_resolve_and_get_m(self):
        """resolve_and_get_m combines resolve and get in one operation."""
        collection = CardCollection(db_path=":memory:")
        card = MCard("Combined Operation")
        collection.add_with_handle(card, "combined")

        result = collection.resolve_and_get_m("combined")
        assert result.is_just
        assert result.value.get_content(as_text=True) == "Combined Operation"
