"""Deterministic Tier 2 routing tests for the course concierge contract."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import re

import pytest


PLUGIN_ROOT = Path(__file__).resolve().parent.parent
CLAUDE_PATH = PLUGIN_ROOT / "CLAUDE.md"
CONCIERGE_PATH = PLUGIN_ROOT / "skills" / "course-concierge" / "SKILL.md"


def _read(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def _extract_section(text: str, start_marker: str, end_marker: str) -> str:
    start = text.index(start_marker)
    end = text.index(end_marker, start)
    return text[start:end]


def _parse_markdown_rows(section: str) -> list[list[str]]:
    rows = []
    for line in section.splitlines():
        stripped = line.strip()
        if not stripped.startswith("|"):
            continue
        columns = [column.strip() for column in stripped.strip("|").split("|")]
        if not columns or set(columns[0]) == {"-"}:
            continue
        if columns[0].lower() in {"family", "label", "scope", "user request"}:
            continue
        rows.append(columns)
    return rows


def parse_intent_families(text: str) -> dict[str, list[str]]:
    section = _extract_section(text, "#### Intent Families", "#### Hard Scope Rules")
    family_map = {}
    for family_cell, _language, skills_cell in _parse_markdown_rows(section):
        family = re.sub(r"\*\*", "", family_cell).strip()
        skills = re.findall(r"`([^`]+)`", skills_cell)
        family_map[family] = skills
    return family_map


def parse_concierge_intent_families(text: str) -> dict[str, list[str]]:
    section = _extract_section(text, "## Step 5: Intent Families", "### Notes on Family Boundaries")
    family_map = {}
    for family_cell, _language, skills_cell in _parse_markdown_rows(section):
        family = re.sub(r"\*\*", "", family_cell).strip()
        skills = re.findall(r"`([^`]+)`", skills_cell)
        family_map[family] = skills
    return family_map


def parse_hard_scope_rules(text: str) -> list[str]:
    section = _extract_section(text, "#### Hard Scope Rules", "#### Routing Precedence")
    return [
        line.strip()[2:].strip()
        for line in section.splitlines()
        if line.strip().startswith("- ")
    ]


@dataclass(frozen=True)
class RouteDecision:
    prompt: str
    skill: str
    family: str | None
    mode: str


class RouterSimulator:
    """Pure-logic router that mirrors the markdown routing contract."""

    def __init__(self, claude_text: str, concierge_text: str) -> None:
        self.family_map = parse_intent_families(claude_text)
        self.skill_to_family = {
            skill: family
            for family, skills in self.family_map.items()
            for skill in skills
        }
        self.hard_scope_rules = parse_hard_scope_rules(claude_text)
        self.concierge_text = concierge_text.lower()

    def route(self, prompt: str) -> RouteDecision:
        raw_prompt = prompt
        text = prompt.strip().lower()

        match = re.match(r"^/([a-z][a-z-]+)\b", text)
        if match:
            skill = match.group(1)
            return RouteDecision(
                prompt=raw_prompt,
                skill=skill,
                family=self.skill_to_family.get(skill),
                mode="direct-skill-invocation",
            )

        if self._is_mockup_request(text):
            return self._decision(raw_prompt, "course-mockup", "hard-scope")

        if "remediation brief" in text:
            return self._decision(raw_prompt, "bulk-edit", "hard-scope")

        if self._is_plugin_feedback_request(text):
            return self._decision(raw_prompt, "plugin-feedback", "hard-scope")

        if any(
            phrase in text
            for phrase in (
                "show backlog",
                "list pending findings",
                "backlog cleanup",
                "what's pending",
                "whats pending",
            )
        ):
            return self._decision(raw_prompt, "backlog", "hard-scope")

        if any(phrase in text for phrase in ("stage changes", "rollback")) or (
            "push" in text and "canvas" in text
        ):
            return self._decision(raw_prompt, "staging", "clear-specific-request")

        if self._is_single_item_quiz_metadata_edit(text):
            return self._decision(raw_prompt, "quiz-generator", "hard-scope")

        if self._is_course_configuration_request(text):
            return self._decision(raw_prompt, "course-config", "hard-scope")

        if self._is_resource_request(text):
            return self._decision(raw_prompt, "resource-discovery", "hard-scope")

        if self._is_media_upload_request(text):
            return self._decision(raw_prompt, "media-upload", "hard-scope")

        if self._is_image_request(text):
            return self._decision(raw_prompt, "images", "hard-scope")

        if self._is_custom_visual_request(text):
            return self._decision(raw_prompt, "visual-builder", "hard-scope")

        if self._is_accessibility_remediation_request(text):
            return self._decision(raw_prompt, "bulk-edit", "hard-scope")

        for matcher, skill in (
            (self._is_assurance_request, "assurance-audit"),
            (self._is_alignment_request, "alignment"),
            (self._is_build_request, "course-build"),
            (self._is_syllabus_import_request, "syllabus-generator"),
            (self._is_needs_analysis_request, "needs-analysis"),
            (self._is_plan_import_request, "course-build"),
            (lambda value: "create a quiz" in value or re.search(r"\bquiz\b", value) is not None, "quiz-generator"),
            (lambda value: "make a rubric" in value or "rubric" in value, "rubric-creator"),
            (lambda value: "write a discussion" in value or "discussion" in value, "discussion-generator"),
            (lambda value: "generate an assignment" in value or "assignment" in value, "assignment-generator"),
            (lambda value: "lecture scripts" in value, "instructor-guide"),
            (lambda value: "audit my course" in value, "course-audit"),
            (
                lambda value: any(
                    phrase in value
                    for phrase in (
                        "accessible",
                        "accessibility audit",
                        "wcag compliant",
                        "wcag 2.1",
                        "accessibility compliance",
                        "check accessibility",
                    )
                ),
                "course-audit",
            ),
            (lambda value: "ready to launch" in value or "is this ready" in value, "course-audit"),
            (lambda value: "review the pedagogy" in value or "pedagogy" in value, "course-audit"),
            (lambda value: "fix accessibility issues" in value or "clean up this course" in value, "bulk-edit"),
            (lambda value: "update module" in value or "edit this page" in value or "update this page" in value, "update-module"),
            (lambda value: "browse modules" in value or "show me the course" in value, "canvas-nav"),
            (lambda value: "copy from another course" in value or "copy course" in value or "copy-course" in value, "course-transfer"),
            (
                lambda value: any(
                    phrase in value
                    for phrase in (
                        "save as template",
                        "save this as a template",
                        "save as a template",
                        "list templates",
                    )
                ),
                "canvas-nav",
            ),
        ):
            if matcher(text):
                return self._decision(raw_prompt, skill, "clear-specific-request")

        return RouteDecision(
            prompt=raw_prompt,
            skill="course-concierge",
            family=None,
            mode="ambiguous-or-open-ended",
        )

    def _decision(self, prompt: str, skill: str, mode: str) -> RouteDecision:
        return RouteDecision(
            prompt=prompt,
            skill=skill,
            family=self.skill_to_family.get(skill),
            mode=mode,
        )

    @staticmethod
    def _is_mockup_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "mockup",
                "faculty preview",
                "show faculty what this would look like",
                "prototype",
            )
        )

    @staticmethod
    def _is_single_item_quiz_metadata_edit(text: str) -> bool:
        return (
            "set due date" in text
            and "quiz" in text
            and "all assignments" not in text
            and "all quizzes" not in text
        )

    @staticmethod
    def _is_course_configuration_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "publish all modules",
                "set due dates for all assignments",
                "configure assignment groups",
                "hide the files tab",
                "set late policy",
                "add turnitin",
            )
        )

    @staticmethod
    def _is_resource_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "find academic resources",
                "curate readings",
                "academic papers",
                "journal articles",
                "research readings",
            )
        )

    @staticmethod
    def _is_media_upload_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "upload this pdf",
                "upload this video",
                "embed this pdf",
                "embed this video",
                "add this image to the page",
            )
        )

    @staticmethod
    def _is_image_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "find images",
                "generate images",
                "image search",
                "image generation",
            )
        )

    @staticmethod
    def _is_custom_visual_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "make a diagram",
                "custom chart",
                "make a chart",
                "infographic",
            )
        )

    @staticmethod
    def _is_build_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "build a course",
                "build me a course",
                "create a course from scratch",
                "start from scratch",
            )
        ) or ("build me" in text and "course" in text and "from scratch" in text)

    @staticmethod
    def _is_syllabus_import_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "import this syllabus",
                "import a syllabus",
                "import the syllabus",
            )
        ) or (("import" in text or "parse" in text) and "syllabus pdf" in text)

    @staticmethod
    def _is_needs_analysis_request(text: str) -> bool:
        return "run needs analysis" in text or "run a needs analysis" in text

    @staticmethod
    def _is_plan_import_request(text: str) -> bool:
        return "planning map" in text or "import this planning map" in text

    @staticmethod
    def _is_accessibility_remediation_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "make this course accessible",
                "make this module accessible",
                "make this page accessible",
                "fix a11y",
                "fix a11y issues",
            )
        )

    @staticmethod
    def _is_alignment_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "is my course aligned",
                "check clo coverage",
                "do my assessments map to objectives",
                "alignment audit",
                "alignment check",
            )
        )

    @staticmethod
    def _is_assurance_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "ai-proof",
                "bypass with ai",
                "bypass this course with ai",
                "assessment assurance",
                "assurance audit",
                "aias",
                "authentic assessment",
                "certify learning",
                "substitutability",
            )
        )

    @staticmethod
    def _is_plugin_feedback_request(text: str) -> bool:
        return any(
            phrase in text
            for phrase in (
                "leave feedback",
                "report a bug",
                "suggest an improvement",
                "plugin feedback",
            )
        )


CLAUDE_TEXT = _read(CLAUDE_PATH)
CONCIERGE_TEXT = _read(CONCIERGE_PATH)
SIMULATOR = RouterSimulator(CLAUDE_TEXT, CONCIERGE_TEXT)
CLAUDE_FAMILY_MAP = parse_intent_families(CLAUDE_TEXT)
CONCIERGE_FAMILY_MAP = parse_concierge_intent_families(CONCIERGE_TEXT)
HARD_SCOPE_RULES = parse_hard_scope_rules(CLAUDE_TEXT)

FAMILY_CASES = [
    ("build a course for BIO 101", "course-build", "Build & Import"),
    ("build me a course for BIO 101", "course-build", "Build & Import"),
    ("build me a BIO 181 course from scratch", "course-build", "Build & Import"),
    ("create a course from scratch for BIO 101", "course-build", "Build & Import"),
    ("import this syllabus", "syllabus-generator", "Build & Import"),
    ("import a syllabus for this course", "syllabus-generator", "Build & Import"),
    ("import the syllabus for this course", "syllabus-generator", "Build & Import"),
    ("please parse this syllabus pdf", "syllabus-generator", "Build & Import"),
    ("run needs analysis", "needs-analysis", "Build & Import"),
    ("turn this planning map into a course", "course-build", "Build & Import"),
    ("create a mockup for faculty", "course-mockup", "Prototype & Faculty Review"),
    ("revise the mockup", "course-mockup", "Prototype & Faculty Review"),
    ("show faculty what this would look like", "course-mockup", "Prototype & Faculty Review"),
    ("create a quiz for Module 3", "quiz-generator", "Create Content"),
    ("make a rubric", "rubric-creator", "Create Content"),
    ("write a discussion", "discussion-generator", "Create Content"),
    ("generate an assignment", "assignment-generator", "Create Content"),
    ("create lecture scripts", "instructor-guide", "Create Content"),
    ("find images for Module 2", "images", "Visuals & Media"),
    ("make a diagram", "visual-builder", "Visuals & Media"),
    ("upload this PDF", "media-upload", "Visuals & Media"),
    ("embed this video", "media-upload", "Visuals & Media"),
    ("find academic resources", "resource-discovery", "Resources & Research"),
    ("curate readings for Module 1", "resource-discovery", "Resources & Research"),
    ("audit my course", "course-audit", "Review"),
    ("is this accessible?", "course-audit", "Review"),
    ("check if this is wcag compliant", "course-audit", "Review"),
    ("review this for wcag 2.1", "course-audit", "Review"),
    ("run an accessibility compliance check", "course-audit", "Review"),
    ("check accessibility for this module", "course-audit", "Review"),
    ("is this ready to launch?", "course-audit", "Review"),
    ("review the pedagogy", "course-audit", "Review"),
    ("is my course aligned?", "alignment", "Review"),
    ("check CLO coverage", "alignment", "Review"),
    ("do my assessments map to objectives", "alignment", "Review"),
    ("run an alignment audit", "alignment", "Review"),
    ("fix accessibility issues", "bulk-edit", "Edit & Remediate"),
    ("fix a11y issues", "bulk-edit", "Edit & Remediate"),
    ("make this course accessible", "bulk-edit", "Edit & Remediate"),
    ("stage changes", "staging", "Edit & Remediate"),
    ("push these changes to canvas", "staging", "Edit & Remediate"),
    ("rollback", "staging", "Edit & Remediate"),
    ("use this remediation brief", "bulk-edit", "Edit & Remediate"),
    ("publish all modules", "course-config", "Configure Course"),
    ("set due dates for all assignments", "course-config", "Configure Course"),
    ("hide the Files tab", "course-config", "Configure Course"),
    ("browse modules", "canvas-nav", "Navigate & Reuse"),
    ("copy from another course", "course-transfer", "Navigate & Reuse"),
    ("copy course content", "course-transfer", "Navigate & Reuse"),
    ("save as template", "canvas-nav", "Navigate & Reuse"),
    ("save as a template", "canvas-nav", "Navigate & Reuse"),
    ("save this as a template", "canvas-nav", "Navigate & Reuse"),
    ("leave feedback", "plugin-feedback", "Meta"),
    ("report a bug", "plugin-feedback", "Meta"),
    ("suggest an improvement", "plugin-feedback", "Meta"),
    ("plugin feedback: the build flow is confusing", "plugin-feedback", "Meta"),
    ("show backlog", "backlog", "Backlog"),
    ("list pending findings", "backlog", "Backlog"),
    ("backlog cleanup", "backlog", "Backlog"),
    ("what's pending on this course", "backlog", "Backlog"),
    ("is my course AI-proof?", "assurance-audit", "Assurance"),
    ("can students bypass this course with AI?", "assurance-audit", "Assurance"),
    ("run an assessment assurance audit", "assurance-audit", "Assurance"),
    ("does this course certify learning?", "assurance-audit", "Assurance"),
]

HARD_SCOPE_CASES = [
    ("Set due date on the Module 2 quiz", "quiz-generator"),
    ("Set due dates for all assignments", "course-config"),
    ("Create a mockup for this course", "course-mockup"),
    ("Find academic papers on cognition", "resource-discovery"),
    ("Make a custom chart for the lesson", "visual-builder"),
]

DIRECT_INVOCATION_CASES = [
    ("/quiz-generator", "quiz-generator"),
    ("/course-build Create BIO 101", "course-build"),
    ("/course-audit", "course-audit"),
]

AMBIGUOUS_CASES = [
    "help me with this course",
    "I want to improve my course",
    "do something with my Canvas",
    "",
    "what should I work on next?",
]


def test_intent_family_table_parses_all_expected_families() -> None:
    assert len(CLAUDE_FAMILY_MAP) == 12
    assert set(CLAUDE_FAMILY_MAP) == {
        "Build & Import",
        "Prototype & Faculty Review",
        "Create Content",
        "Visuals & Media",
        "Resources & Research",
        "Review",
        "Assurance",
        "Edit & Remediate",
        "Configure Course",
        "Navigate & Reuse",
        "Meta",
        "Backlog",
    }


def test_concierge_family_table_matches_claude_sets() -> None:
    assert set(CONCIERGE_FAMILY_MAP) == set(CLAUDE_FAMILY_MAP)
    for family, skills in CLAUDE_FAMILY_MAP.items():
        assert set(CONCIERGE_FAMILY_MAP[family]) == set(skills)


def test_required_hard_scope_rules_are_documented() -> None:
    rules_blob = "\n".join(HARD_SCOPE_RULES).lower()
    for required_phrase in (
        "single-item metadata edit",
        "batch or whole-course settings",
        "mockup/prototype request",
        "academic resource finding",
        "file upload/embed",
        "image search or image generation",
        "custom diagrams, charts, or infographics",
    ):
        assert required_phrase in rules_blob


@pytest.mark.parametrize(("prompt", "expected_skill", "expected_family"), FAMILY_CASES)
def test_family_routing_battery(prompt: str, expected_skill: str, expected_family: str) -> None:
    decision = SIMULATOR.route(prompt)

    assert decision.skill == expected_skill
    assert decision.family == expected_family


@pytest.mark.parametrize(("prompt", "expected_skill"), HARD_SCOPE_CASES)
def test_hard_scope_rules_take_precedence(prompt: str, expected_skill: str) -> None:
    decision = SIMULATOR.route(prompt)

    assert decision.skill == expected_skill
    assert decision.mode in {"hard-scope", "clear-specific-request"}


@pytest.mark.parametrize(("prompt", "expected_skill"), DIRECT_INVOCATION_CASES)
def test_direct_skill_invocation_bypasses_concierge(prompt: str, expected_skill: str) -> None:
    decision = SIMULATOR.route(prompt)

    assert decision.skill == expected_skill
    assert decision.mode == "direct-skill-invocation"


@pytest.mark.parametrize("prompt", AMBIGUOUS_CASES)
def test_ambiguous_prompts_route_to_concierge(prompt: str) -> None:
    decision = SIMULATOR.route(prompt)

    assert decision.skill == "course-concierge"
    assert decision.mode == "ambiguous-or-open-ended"


def test_meta_feedback_takes_precedence_over_other_keywords() -> None:
    decision = SIMULATOR.route("Report a bug — the quiz generator crashed")

    assert decision.skill == "plugin-feedback"
    assert decision.family == "Meta"
    assert decision.mode == "hard-scope"


def test_all_intent_families_have_at_least_one_prompt() -> None:
    covered_families = {expected_family for _, _, expected_family in FAMILY_CASES}

    assert covered_families == set(CLAUDE_FAMILY_MAP)


def test_prompt_accuracy_meets_tier_2_threshold() -> None:
    prompt_cases = (
        [(prompt, expected_skill) for prompt, expected_skill, _family in FAMILY_CASES]
        + HARD_SCOPE_CASES
        + DIRECT_INVOCATION_CASES
        + [(prompt, "course-concierge") for prompt in AMBIGUOUS_CASES]
    )

    correct = 0
    for prompt, expected_skill in prompt_cases:
        if SIMULATOR.route(prompt).skill == expected_skill:
            correct += 1

    accuracy = correct / len(prompt_cases)

    assert len(prompt_cases) >= 25
    assert correct >= 22
    assert accuracy >= 0.90


def test_family_coverage_percent_is_complete() -> None:
    covered_families = {expected_family for _, _, expected_family in FAMILY_CASES}
    coverage_percent = len(covered_families) / len(CLAUDE_FAMILY_MAP) * 100

    assert coverage_percent == 100.0
