"""
Tests for context_curation.py — IDE-injected context sanitization and policy.
"""

import pytest

from api.services.complete.context_curation import (
    CursorCurationPolicy,
    normalize_pathish,
    is_doc_path,
    is_vendor_or_generated_path,
    is_reportlike_doc,
    choose_cursor_curation_policy,
    should_exclude_path_from_recent_hint,
    should_exclude_path_from_ide_proof_exempt_set,
    filter_ide_context_paths,
    sanitize_cursor_injected_text,
    sanitize_cursor_injected_blocks_in_messages,
    prune_cursor_ambient_context_messages,
    build_cursor_workspace_hint,
)
from api.models import CompleteRequest


def _msg(role, content):
    return CompleteRequest.Message(role=role, content=content)


def _default_policy():
    return CursorCurationPolicy(
        name="default",
        redact_injected=True,
        include_docs_in_recent_hint=True,
        include_reportlike_docs_in_recent_hint=False,
        docs_are_proof_exempt=False,
        reportlike_docs_are_proof_exempt=False,
    )


def _strict_policy():
    return CursorCurationPolicy(
        name="strict",
        redact_injected=True,
        include_docs_in_recent_hint=False,
        include_reportlike_docs_in_recent_hint=False,
        docs_are_proof_exempt=False,
        reportlike_docs_are_proof_exempt=False,
    )


def _permissive_policy():
    return CursorCurationPolicy(
        name="permissive",
        redact_injected=False,
        include_docs_in_recent_hint=True,
        include_reportlike_docs_in_recent_hint=True,
        docs_are_proof_exempt=True,
        reportlike_docs_are_proof_exempt=True,
    )


# ===========================================================================
# Path classification
# ===========================================================================

class TestPathClassification:
    def test_normalize_pathish(self):
        assert normalize_pathish("foo\\bar\\baz") == "foo/bar/baz"
        assert normalize_pathish("foo//bar///baz") == "foo/bar/baz"
        assert normalize_pathish("") == ""
        assert normalize_pathish(None) == ""

    def test_is_doc_path(self):
        assert is_doc_path("docs/README.md") is True
        assert is_doc_path("src/utils.py") is False
        assert is_doc_path("doc/guide.md") is True
        assert is_doc_path("CHANGELOG.md") is True

    def test_is_vendor_or_generated(self):
        # Paths need leading / or /segment/ for the segment matcher
        assert is_vendor_or_generated_path("/project/node_modules/foo/index.js") is True
        assert is_vendor_or_generated_path("/project/venv/lib/site-packages/foo.py") is True
        assert is_vendor_or_generated_path("src/app.py") is False
        assert is_vendor_or_generated_path("/project/.git/config") is True
        assert is_vendor_or_generated_path("/project/dist/bundle.js") is True

    def test_is_reportlike_doc(self):
        # The regex uses \b word boundaries, so underscored names like
        # "bug_audit_report.md" won't match (underscore is a word char).
        # Hyphenated or space-separated names do match.
        assert is_reportlike_doc("/project/docs/bug-audit-report.md") is True
        assert is_reportlike_doc("/project/docs/changelog.md") is True
        assert is_reportlike_doc("README.md") is False
        assert is_reportlike_doc("src/report.py") is False  # not a doc path
        assert is_reportlike_doc("/project/docs/security-review.md") is True
        assert is_reportlike_doc("/project/docs/api_guide.md") is False


# ===========================================================================
# Policy selection
# ===========================================================================

class TestPolicySelection:
    def _make_request(self, **kwargs):
        class FakeRequest:
            pass
        r = FakeRequest()
        for k, v in kwargs.items():
            setattr(r, k, v)
        return r

    def test_bug_review_gets_strict(self):
        req = self._make_request()
        policy = choose_cursor_curation_policy(
            complete_request=req,
            bug_review_intent=True,
            repo_overview_intent=False,
            high_rigor_intent=False,
            deep_change_intent=False,
        )
        assert policy.name == "strict"

    def test_default_for_normal_request(self):
        req = self._make_request()
        policy = choose_cursor_curation_policy(
            complete_request=req,
            bug_review_intent=False,
            repo_overview_intent=False,
            high_rigor_intent=False,
            deep_change_intent=False,
        )
        assert policy.name == "default"

    def test_explicit_override(self):
        req = self._make_request(context_curation_policy="permissive")
        policy = choose_cursor_curation_policy(
            complete_request=req,
            bug_review_intent=True,
            repo_overview_intent=False,
            high_rigor_intent=False,
            deep_change_intent=False,
        )
        assert policy.name == "permissive"


# ===========================================================================
# Path exclusion
# ===========================================================================

class TestPathExclusion:
    def test_vendor_always_excluded(self):
        policy = _default_policy()
        assert should_exclude_path_from_recent_hint("/project/node_modules/foo.js", policy=policy) is True

    def test_reportlike_excluded_by_default(self):
        policy = _default_policy()
        assert should_exclude_path_from_recent_hint("/project/docs/bug-audit-report.md", policy=policy) is True

    def test_reportlike_included_in_permissive(self):
        policy = _permissive_policy()
        assert should_exclude_path_from_recent_hint("/project/docs/BUG_AUDIT_REPORT.md", policy=policy) is False

    def test_docs_excluded_in_strict(self):
        policy = _strict_policy()
        assert should_exclude_path_from_recent_hint("docs/guide.md", policy=policy) is True

    def test_source_code_never_excluded(self):
        for p in [_default_policy(), _strict_policy(), _permissive_policy()]:
            assert should_exclude_path_from_recent_hint("src/app.py", policy=p) is False

    def test_filter_ide_context_paths(self):
        policy = _default_policy()
        paths = ["src/app.py", "/project/node_modules/foo.js", "/project/docs/BUG_AUDIT_REPORT.md", "src/utils.ts"]
        filtered = filter_ide_context_paths(paths, policy=policy)
        assert "src/app.py" in filtered
        assert "src/utils.ts" in filtered
        assert "/project/node_modules/foo.js" not in filtered


# ===========================================================================
# Sanitization
# ===========================================================================

class TestSanitization:
    def test_sanitize_xml_blocks(self):
        policy = _strict_policy()
        text = (
            "<open_and_recently_viewed_files>\n"
            "- /project/docs/guide.md\n"
            "- /project/src/app.py\n"
            "</open_and_recently_viewed_files>"
        )
        result = sanitize_cursor_injected_text(text, policy=policy)
        assert "/project/src/app.py" in result
        assert "/project/docs/guide.md" not in result

    def test_sanitize_preserves_non_cursor_content(self):
        policy = _strict_policy()
        text = "This is a normal message with no cursor blocks."
        result = sanitize_cursor_injected_text(text, policy=policy)
        assert result == text

    def test_sanitize_messages(self):
        policy = _strict_policy()
        msgs = [
            _msg("system", "<open_and_recently_viewed_files>\n- /project/docs/bugs.md\n</open_and_recently_viewed_files>"),
            _msg("user", "Find bugs"),
        ]
        result = sanitize_cursor_injected_blocks_in_messages(msgs, policy=policy)
        assert len(result) == 2
        assert "bugs.md" not in (result[0].content or "")


# ===========================================================================
# Pruning
# ===========================================================================

class TestPruning:
    def test_prune_keeps_most_recent(self):
        msgs = [
            _msg("system", "<open_and_recently_viewed_files>block1</open_and_recently_viewed_files>"),
            _msg("user", "hi"),
            _msg("system", "<open_and_recently_viewed_files>block2</open_and_recently_viewed_files>"),
            _msg("user", "do something"),
            _msg("system", "<open_and_recently_viewed_files>block3</open_and_recently_viewed_files>"),
        ]
        result = prune_cursor_ambient_context_messages(msgs, keep_last_per_tag=1, max_total_blocks=1)
        assert len(result) == 5
        block_contents = [
            m.content for m in result
            if "open_and_recently_viewed_files" in (m.content or "")
        ]
        assert any("block3" in c for c in block_contents)

    def test_no_cursor_blocks_unchanged(self):
        msgs = [_msg("user", "hello"), _msg("assistant", "hi")]
        result = prune_cursor_ambient_context_messages(msgs)
        assert len(result) == 2


# ===========================================================================
# Workspace hint
# ===========================================================================

class TestWorkspaceHint:
    def test_build_hint_with_data(self):
        class FakeCtx:
            workspace_path = "/project"
            focused_file = "src/app.py"
            focused_line = 42
            focused_total_lines = 200
            all_changed_files = ["src/app.py", "src/utils.py"]
            recently_viewed_files = ["src/old.py"]
            os_name = "darwin"

        policy = _default_policy()
        hint = build_cursor_workspace_hint(ws_ctx=FakeCtx(), policy=policy)
        assert hint is not None
        assert "src/app.py" in hint
        assert "WORKSPACE CONTEXT" in hint

    def test_build_hint_empty_ctx(self):
        class EmptyCtx:
            pass

        policy = _default_policy()
        hint = build_cursor_workspace_hint(ws_ctx=EmptyCtx(), policy=policy)
        assert hint is None
