"""
Unit tests for models.py.

Pydantic models are the first validation layer. Tests confirm
that every required field is enforced and that frozen models
reject mutation.
"""

from __future__ import annotations

import pytest
from pydantic import ValidationError

from active_claude_github_mcp.models import (
    ClassifyResult,
    DiscussionCategories,
    GithubConfig,
    OperationError,
    RelatedRepo,
    ThemeChangeResult,
)


class TestGithubConfig:
    def test_valid_config(self, valid_config_dict):
        config = GithubConfig.model_validate(valid_config_dict)
        assert config.owner == "telejester"

    def test_missing_owner_raises(self, valid_config_dict):
        del valid_config_dict["owner"]
        with pytest.raises(ValidationError):
            GithubConfig.model_validate(valid_config_dict)

    def test_wrong_skill_raises(self, valid_config_dict):
        valid_config_dict["skill"] = "wrong-skill"
        with pytest.raises(ValidationError):
            GithubConfig.model_validate(valid_config_dict)

    def test_short_project_discussion_id_raises(self, valid_config_dict):
        valid_config_dict["project_discussion_id"] = "short"
        with pytest.raises(ValidationError, match="suspiciously short"):
            GithubConfig.model_validate(valid_config_dict)

    def test_frozen_rejects_mutation(self, valid_config_dict):
        config = GithubConfig.model_validate(valid_config_dict)
        with pytest.raises(Exception):
            config.owner = "other"  # type: ignore[misc]

    def test_extra_fields_rejected(self, valid_config_dict):
        valid_config_dict["unknown_field"] = "value"
        with pytest.raises(ValidationError):
            GithubConfig.model_validate(valid_config_dict)

    def test_full_config_with_all_optional_fields(self, valid_config_dict_full):
        config = GithubConfig.model_validate(valid_config_dict_full)
        assert config.project_board.status_field_id is not None
        assert config.project_board.status_options is not None
        assert config.discussion_categories.Project is not None
        assert config.discussion_categories.Architecture.note is not None

    def test_optional_status_fields_accepted(self, valid_config_dict):
        valid_config_dict["project_board"]["status_field_id"] = "PVTSSF_abc123abcd"
        valid_config_dict["project_board"]["status_options"] = {"Backlog": "aaa"}
        config = GithubConfig.model_validate(valid_config_dict)
        assert config.project_board.status_field_id == "PVTSSF_abc123abcd"
        assert config.project_board.status_options == {"Backlog": "aaa"}

    def test_project_category_accepted(self, valid_config_dict):
        valid_config_dict["discussion_categories"]["Project"] = {
            "id": "DIC_kwDOSIO9Xs4C7UzY",
            "isAnswerable": False,
        }
        config = GithubConfig.model_validate(valid_config_dict)
        assert config.discussion_categories.Project is not None

    def test_note_on_category_accepted(self, valid_config_dict):
        valid_config_dict["discussion_categories"]["Architecture"]["note"] = "Q&A"
        config = GithubConfig.model_validate(valid_config_dict)
        assert config.discussion_categories.Architecture.note == "Q&A"


class TestDiscussionCategories:
    def test_empty_category_id_raises(self, valid_config_dict):
        valid_config_dict["discussion_categories"]["Architecture"]["id"] = ""
        with pytest.raises(ValidationError, match="empty ID"):
            GithubConfig.model_validate(valid_config_dict)

    def test_project_category_empty_id_raises(self, valid_config_dict):
        valid_config_dict["discussion_categories"]["Project"] = {
            "id": "",
            "isAnswerable": False,
        }
        with pytest.raises(ValidationError, match="empty ID"):
            GithubConfig.model_validate(valid_config_dict)

    def test_extra_category_accepted(self, valid_config_dict):
        valid_config_dict["discussion_categories"]["General"] = {
            "id": "DIC_kwDOSIO9Xs4C7UzZ",
            "isAnswerable": False,
            "note": "Admin / session records",
        }
        config = GithubConfig.model_validate(valid_config_dict)
        extras = config.discussion_categories.model_extra or {}
        assert "General" in extras
        assert extras["General"].id == "DIC_kwDOSIO9Xs4C7UzZ"
        assert extras["General"].is_answerable is False

    def test_extra_category_empty_id_raises(self, valid_config_dict):
        valid_config_dict["discussion_categories"]["General"] = {
            "id": "",
            "isAnswerable": False,
        }
        with pytest.raises(ValidationError, match="empty ID"):
            GithubConfig.model_validate(valid_config_dict)

    def test_extra_category_missing_fields_raises(self, valid_config_dict):
        valid_config_dict["discussion_categories"]["General"] = {
            "id": "DIC_kwDOSIO9Xs4C7UzZ",
        }
        with pytest.raises(ValidationError, match="is invalid"):
            GithubConfig.model_validate(valid_config_dict)


class TestClassifyResult:
    def test_valid_discussion(self):
        result = ClassifyResult(
            destination="discussion",
            category="Architecture",
            issue_label=None,
            suggested_title="Auth design",
            confidence="high",
        )
        assert result.destination == "discussion"

    def test_invalid_destination_raises(self):
        with pytest.raises(ValidationError):
            ClassifyResult(
                destination="unknown",  # type: ignore[arg-type]
                category=None,
                issue_label=None,
                suggested_title="Title",
                confidence="high",
            )

    def test_invalid_confidence_raises(self):
        with pytest.raises(ValidationError):
            ClassifyResult(
                destination="discussion",
                category=None,
                issue_label=None,
                suggested_title="Title",
                confidence="very_sure",  # type: ignore[arg-type]
            )


class TestOperationError:
    def test_success_field_is_always_false(self):
        err = OperationError(
            success=False,
            error="timeout",
            buffered=True,
            buffer_file=".claude/pending-posts.md",
            retry_tool="post_turn",
            retry_args={"thread_id": "D_abc"},
        )
        assert err.success is False
        assert err.manual_step is None

    def test_cannot_set_success_true(self):
        with pytest.raises(ValidationError):
            OperationError(
                success=True,  # type: ignore[arg-type]
                error="",
                buffered=False,
                buffer_file=None,
                retry_tool=None,
                retry_args=None,
            )

    def test_manual_step_populated_for_protocol_gate(self):
        # #98 — cousin preflight block surfaces a manual_step rather
        # than a buffered error.
        err = OperationError(
            success=False,
            error="cousin-repo mutation blocked: other-org/foo",
            buffered=False,
            buffer_file=None,
            retry_tool=None,
            retry_args=None,
            manual_step=(
                "Cross-org mutation on 'other-org/foo' requires "
                "explicit user permission."
            ),
        )
        assert err.manual_step is not None
        assert "explicit user permission" in err.manual_step


class TestRelatedRepo:
    # #98 — RelatedRepo extends config schema from 3 to 6 typed
    # relationships, with `contribution_target` parsed-but-deprecated.

    def test_all_six_types_accepted(self):
        for rel in (
            "subsystem",
            "publish_target",
            "sibling",
            "cousin",
            "neighbor",
            "foreign",
        ):
            entry = RelatedRepo(repo="org/name", relationship=rel)
            assert entry.relationship == rel

    def test_contribution_target_accepted_deprecated(self):
        # Parses cleanly so existing configs load; the deprecation
        # warning is emitted by config.load_config, not by the model.
        entry = RelatedRepo(repo="org/name", relationship="contribution_target")
        assert entry.relationship == "contribution_target"

    def test_invalid_relationship_rejected(self):
        with pytest.raises(ValidationError):
            RelatedRepo(
                repo="org/name",
                relationship="partner",  # type: ignore[arg-type]
            )

    def test_repo_without_slash_rejected(self):
        with pytest.raises(ValidationError, match="owner/name"):
            RelatedRepo(repo="just-a-name", relationship="sibling")

    def test_repo_with_multiple_slashes_rejected(self):
        with pytest.raises(ValidationError, match="owner/name"):
            RelatedRepo(repo="a/b/c", relationship="sibling")

    def test_repo_with_empty_parts_rejected(self):
        with pytest.raises(ValidationError, match="owner/name"):
            RelatedRepo(repo="/name", relationship="sibling")
        with pytest.raises(ValidationError, match="owner/name"):
            RelatedRepo(repo="org/", relationship="sibling")

    def test_description_optional(self):
        with_desc = RelatedRepo(
            repo="org/name", relationship="sibling", description="A sibling"
        )
        no_desc = RelatedRepo(repo="org/name", relationship="sibling")
        assert with_desc.description == "A sibling"
        assert no_desc.description is None

    def test_extra_fields_rejected(self):
        with pytest.raises(ValidationError):
            RelatedRepo.model_validate(
                {
                    "repo": "org/name",
                    "relationship": "sibling",
                    "nope": "extra",
                }
            )


class TestGithubConfigRelatedRepos:
    # #98 — related_repos is optional; empty default means existing
    # bootstrap configs continue to load unchanged.

    def test_related_repos_optional_defaults_empty(self, valid_config_dict):
        config = GithubConfig.model_validate(valid_config_dict)
        assert config.related_repos == []

    def test_related_repos_populated(self, valid_config_dict):
        valid_config_dict["related_repos"] = [
            {
                "repo": "telejester-claude-skills/claude-skills-mcp-dev",
                "relationship": "subsystem",
                "description": "MCP server source",
            },
            {
                "repo": "other-org/shared-tool",
                "relationship": "cousin",
                "description": "Cross-org collaborator",
            },
        ]
        config = GithubConfig.model_validate(valid_config_dict)
        assert len(config.related_repos) == 2
        assert config.related_repos[0].relationship == "subsystem"
        assert config.related_repos[1].relationship == "cousin"

    def test_find_related_repo_match(self, valid_config_dict):
        valid_config_dict["related_repos"] = [
            {"repo": "other-org/Shared-Tool", "relationship": "cousin"}
        ]
        config = GithubConfig.model_validate(valid_config_dict)
        # Case-insensitive lookup so casing drift in args doesn't miss.
        entry = config.find_related_repo("other-org/shared-tool")
        assert entry is not None
        assert entry.relationship == "cousin"

    def test_find_related_repo_miss(self, valid_config_dict):
        config = GithubConfig.model_validate(valid_config_dict)
        assert config.find_related_repo("nobody/nothing") is None


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