"""Tests for promptkit.prompt module."""

import hashlib
from typing import Dict, List, Optional

import pytest

from promptkit.prompt import Prompt, PromptVersion


# ── PromptVersion tests ────────────────────────────────────────


class TestPromptVersionInit:
    """Tests for PromptVersion construction and auto-fields."""

    def test_auto_generates_content_hash(self) -> None:
        v = PromptVersion(version=1, content="hello world")
        expected = hashlib.sha256(b"hello world").hexdigest()[:12]
        assert v.content_hash == expected

    def test_content_hash_length(self) -> None:
        v = PromptVersion(version=1, content="test content")
        assert len(v.content_hash) == 12

    def test_auto_generates_created_at(self) -> None:
        v = PromptVersion(version=1, content="test")
        assert v.created_at != ""
        assert "T" in v.created_at  # ISO format

    def test_preserves_explicit_created_at(self) -> None:
        v = PromptVersion(version=1, content="test", created_at="2025-01-01T00:00:00+00:00")
        assert v.created_at == "2025-01-01T00:00:00+00:00"

    def test_preserves_explicit_hash(self) -> None:
        v = PromptVersion(version=1, content="test", content_hash="custom123456")
        assert v.content_hash == "custom123456"


class TestPromptVersionVariables:
    """Tests for variable extraction."""

    def test_extracts_single_variable(self) -> None:
        v = PromptVersion(version=1, content="Hello {{name}}")
        assert "name" in v.variables

    def test_extracts_multiple_variables(self) -> None:
        v = PromptVersion(version=1, content="{{greeting}}, {{name}}! Welcome to {{place}}.")
        assert sorted(v.variables) == ["greeting", "name", "place"]

    def test_no_variables_in_plain_text(self) -> None:
        v = PromptVersion(version=1, content="Plain text without variables")
        assert v.variables == []

    def test_deduplicates_variables(self) -> None:
        v = PromptVersion(version=1, content="{{x}} and {{x}} again")
        assert v.variables.count("x") == 1

    def test_variables_sorted(self) -> None:
        v = PromptVersion(version=1, content="{{z_var}} then {{a_var}}")
        assert v.variables == ["a_var", "z_var"]


class TestPromptVersionRender:
    """Tests for Jinja2 rendering."""

    def test_render_with_variables(self) -> None:
        v = PromptVersion(version=1, content="Hello {{name}}")
        assert v.render({"name": "Alice"}) == "Hello Alice"

    def test_render_without_variables_returns_content(self) -> None:
        v = PromptVersion(version=1, content="Static text")
        assert v.render() == "Static text"

    def test_render_with_none_returns_content(self) -> None:
        v = PromptVersion(version=1, content="Static text")
        assert v.render(None) == "Static text"

    def test_render_with_empty_dict_returns_content(self) -> None:
        v = PromptVersion(version=1, content="Static text")
        assert v.render({}) == "Static text"

    def test_render_missing_variable_substitutes_empty(self) -> None:
        v = PromptVersion(version=1, content="Hello {{name}}")
        # Jinja2 default: undefined variables render as empty string
        result = v.render({"wrong_key": "value"})
        assert "name" not in result  # variable not filled with literal name

    def test_render_partial_variables_fills_known(self) -> None:
        v = PromptVersion(version=1, content="{{a}} and {{b}}")
        # Jinja2 default: missing 'b' renders as empty
        result = v.render({"a": "1"})
        assert result.startswith("1 and")


class TestPromptVersionTokenEstimate:
    """Tests for token_estimate method."""

    def test_token_estimate_basic(self) -> None:
        v = PromptVersion(version=1, content="a" * 100)
        assert v.token_estimate() == 25  # 100 // 4

    def test_token_estimate_empty(self) -> None:
        v = PromptVersion(version=1, content="")
        assert v.token_estimate() == 0

    def test_token_estimate_short(self) -> None:
        v = PromptVersion(version=1, content="Hi")
        assert v.token_estimate() == 0  # 2 // 4 == 0


class TestPromptVersionSerialization:
    """Tests for to_dict / from_dict round-trip."""

    def test_to_dict(self) -> None:
        v = PromptVersion(version=1, content="test", message="msg")
        d = v.to_dict()
        assert d["version"] == 1
        assert d["content"] == "test"
        assert d["message"] == "msg"

    def test_round_trip(self) -> None:
        v = PromptVersion(version=2, content="{{x}} template", message="round trip")
        d = v.to_dict()
        v2 = PromptVersion.from_dict(d)
        assert v2.version == v.version
        assert v2.content == v.content
        assert v2.content_hash == v.content_hash
        assert v2.variables == v.variables

    def test_short_hash(self) -> None:
        v = PromptVersion(version=1, content="test")
        assert v.short_hash == v.content_hash[:8]


# ── Prompt tests ───────────────────────────────────────────────


class TestPromptInit:
    """Tests for Prompt construction."""

    def test_new_prompt_has_no_versions(self) -> None:
        p = Prompt(name="test")
        assert p.version_count == 0
        assert p.latest is None

    def test_auto_generates_timestamps(self) -> None:
        p = Prompt(name="test")
        assert p.created_at != ""
        assert p.updated_at != ""


class TestPromptAddVersion:
    """Tests for adding versions."""

    def test_add_version_increments(self) -> None:
        p = Prompt(name="test")
        v1 = p.add_version("Version one")
        v2 = p.add_version("Version two")
        assert v1.version == 1
        assert v2.version == 2

    def test_add_version_auto_tags_latest(self) -> None:
        p = Prompt(name="test")
        p.add_version("content")
        assert p.tags["latest"] == 1
        p.add_version("updated")
        assert p.tags["latest"] == 2

    def test_latest_returns_last_version(self) -> None:
        p = Prompt(name="test")
        p.add_version("first")
        p.add_version("second")
        assert p.latest is not None
        assert p.latest.content == "second"

    def test_version_count(self) -> None:
        p = Prompt(name="test")
        p.add_version("one")
        p.add_version("two")
        p.add_version("three")
        assert p.version_count == 3

    def test_add_version_with_metadata(self) -> None:
        p = Prompt(name="test")
        v = p.add_version("content", metadata={"author": "tester"})
        assert v.metadata["author"] == "tester"


class TestPromptGetVersion:
    """Tests for getting versions by number or tag."""

    def test_get_by_version_number(self, sample_prompt: Prompt) -> None:
        v = sample_prompt.get_version(1)
        assert v is not None
        assert v.version == 1

    def test_get_by_tag(self, sample_prompt: Prompt) -> None:
        v = sample_prompt.get_version(tag="latest")
        assert v is not None
        assert v.version == 2

    def test_get_none_returns_latest(self, sample_prompt: Prompt) -> None:
        v = sample_prompt.get_version()
        assert v is not None
        assert v.version == 2

    def test_get_missing_tag_returns_none(self, sample_prompt: Prompt) -> None:
        assert sample_prompt.get_version(tag="nonexistent") is None

    def test_get_invalid_version_returns_none(self, sample_prompt: Prompt) -> None:
        assert sample_prompt.get_version(999) is None


class TestPromptTagging:
    """Tests for add_tag / remove_tag."""

    def test_add_tag(self, sample_prompt: Prompt) -> None:
        sample_prompt.add_tag("prod", 1)
        assert sample_prompt.tags["prod"] == 1

    def test_add_tag_default_latest(self, sample_prompt: Prompt) -> None:
        sample_prompt.add_tag("staging")
        assert sample_prompt.tags["staging"] == sample_prompt.version_count

    def test_add_tag_invalid_version_raises(self, sample_prompt: Prompt) -> None:
        with pytest.raises(ValueError, match="Version .* not found"):
            sample_prompt.add_tag("bad", 999)

    def test_remove_tag(self, sample_prompt: Prompt) -> None:
        sample_prompt.add_tag("temp", 1)
        assert sample_prompt.remove_tag("temp") is True
        assert "temp" not in sample_prompt.tags

    def test_remove_latest_tag_raises(self, sample_prompt: Prompt) -> None:
        with pytest.raises(ValueError, match="Cannot remove 'latest' tag"):
            sample_prompt.remove_tag("latest")

    def test_remove_nonexistent_tag_returns_false(self, sample_prompt: Prompt) -> None:
        assert sample_prompt.remove_tag("nonexistent") is False


class TestPromptDiff:
    """Tests for Prompt.diff."""

    def test_diff_returns_list(self, sample_prompt: Prompt) -> None:
        result = sample_prompt.diff(1, 2)
        assert isinstance(result, list)

    def test_diff_has_expected_types(self, sample_prompt: Prompt) -> None:
        result = sample_prompt.diff(1, 2)
        types = {item["type"] for item in result}
        assert "header" in types or "add" in types or "remove" in types

    def test_diff_invalid_version_raises(self, sample_prompt: Prompt) -> None:
        with pytest.raises(ValueError):
            sample_prompt.diff(1, 999)

    def test_diff_same_version(self) -> None:
        p = Prompt(name="test")
        p.add_version("same content")
        p.add_version("same content")
        result = p.diff(1, 2)
        # No actual diff lines for identical content
        add_remove = [x for x in result if x["type"] in ("add", "remove")]
        assert len(add_remove) == 0


class TestPromptSerialization:
    """Tests for Prompt.to_dict / from_dict."""

    def test_to_dict(self, sample_prompt: Prompt) -> None:
        d = sample_prompt.to_dict()
        assert d["name"] == "greeting"
        assert len(d["versions"]) == 2

    def test_round_trip(self, sample_prompt: Prompt) -> None:
        d = sample_prompt.to_dict()
        p2 = Prompt.from_dict(d)
        assert p2.name == sample_prompt.name
        assert p2.version_count == sample_prompt.version_count
        assert p2.tags == sample_prompt.tags
