"""Tests for mockup_manager.py — Feature A: Audience metadata + lint.

Covers:
- audience_lint() detects forbidden tokens for faculty-tagged pages
- Clean pages report no violations
- Untagged pages default to student forbidden list
- --strict mode exits 1 when violations exist
- --audience override lints all pages against the specified level
- Word-boundary pattern correctness (cccv1p2 must NOT match; v3 must match)
- save_page() accepts and persists audience kwarg to .meta.json
"""

import json
import os
import shutil
import sys
import unittest
from unittest.mock import patch

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))

import mockup_manager

PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEST_COURSE_ID = "99990"


def _make_course(tmp_path_str, course_id=TEST_COURSE_ID):
    """Patch _mockup_dir to use tmp_path for test isolation."""
    course_mockup = os.path.join(tmp_path_str, "courses", str(course_id), "mockup")
    os.makedirs(os.path.join(course_mockup, "pages"), exist_ok=True)
    os.makedirs(os.path.join(course_mockup, "briefs"), exist_ok=True)
    return course_mockup


class TestAudienceLintDetectsViolations(unittest.TestCase):
    """audience_lint() detects forbidden tokens in faculty-tagged page."""

    def setUp(self):
        import tempfile
        self._tmpdir = tempfile.mkdtemp(prefix="asucb_test_")
        self._orig_mockup_dir = mockup_manager._mockup_dir

        course_dir = _make_course(self._tmpdir)

        def _patched_mockup_dir(course_id, subdir=None):
            parts = [self._tmpdir, "courses", str(course_id), "mockup"]
            if subdir:
                parts.append(subdir)
            path = os.path.join(*parts)
            os.makedirs(path, exist_ok=True)
            return path

        self._patcher = patch.object(
            mockup_manager, "_mockup_dir", side_effect=_patched_mockup_dir)
        self._patcher.start()

        # Faculty page with "v3" and "HPI/ID"
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test Course")
        mockup_manager.save_page(
            TEST_COURSE_ID, "00-redesign-rationale.html",
            "<h2>Redesign Proposal v3</h2><p>Using HPI/ID frameworks.</p>",
            audience="faculty",
        )
        # Student page — clean content
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html",
            "<h2>Module 1 Overview</h2><p>Welcome to the course.</p>",
            audience="student",
        )
        # Untagged page — no audience in meta
        mockup_manager.save_page(
            TEST_COURSE_ID, "m2-lesson.html",
            "<h2>Lesson</h2><p>Normal content. Reference: cccv1p2.</p>",
        )

    def tearDown(self):
        self._patcher.stop()
        shutil.rmtree(self._tmpdir, ignore_errors=True)

    def test_faculty_violation_detected(self):
        result = mockup_manager.audience_lint(TEST_COURSE_ID)
        self.assertTrue(result["ok"])
        self.assertEqual(result["course_id"], str(TEST_COURSE_ID))

        violations = result["violations"]
        faculty_violations = [v for v in violations
                               if v["filename"] == "00-redesign-rationale.html"]
        self.assertEqual(len(faculty_violations), 1,
                         f"Expected 1 violation for faculty page; got {violations}")
        v = faculty_violations[0]
        self.assertEqual(v["audience"], "faculty")
        # Both v3 and HPI/ID should be flagged
        self.assertIn("v3", v["tokens"])
        self.assertIn("HPI/ID", v["tokens"])
        self.assertTrue(len(v["line_hints"]) >= 1)

    def test_clean_student_page_no_violation(self):
        result = mockup_manager.audience_lint(TEST_COURSE_ID)
        student_violations = [v for v in result["violations"]
                               if v["filename"] == "m1-overview.html"]
        self.assertEqual(len(student_violations), 0,
                         "Clean student page should have no violations")

    def test_untagged_page_defaults_to_student(self):
        """Untagged page must be linted against student list (safe default)."""
        # cccv1p2 must NOT trigger the \bv\d+\b pattern (word-boundary test)
        result = mockup_manager.audience_lint(TEST_COURSE_ID)
        untagged = [v for v in result["violations"]
                    if v["filename"] == "m2-lesson.html"]
        self.assertEqual(len(untagged), 0,
                         "cccv1p2 should not match \\bv\\d+\\b (word-boundary test)")

    def test_pages_scanned_count(self):
        result = mockup_manager.audience_lint(TEST_COURSE_ID)
        self.assertEqual(result["pages_scanned"], 3)

    def test_audience_override_applies_to_all_pages(self):
        """--audience faculty should lint ALL pages against faculty list."""
        result = mockup_manager.audience_lint(
            TEST_COURSE_ID, override_audience="faculty")
        # Faculty page still has violations
        faculty_v = [v for v in result["violations"]
                     if v["filename"] == "00-redesign-rationale.html"]
        self.assertEqual(len(faculty_v), 1)
        # Clean m1-overview page should still be clean (it has no faculty-forbidden tokens)
        student_v = [v for v in result["violations"]
                     if v["filename"] == "m1-overview.html"]
        self.assertEqual(len(student_v), 0,
                         "Clean page has no faculty-forbidden tokens")

    def test_audience_id_no_restrictions(self):
        """id audience should never produce violations."""
        result = mockup_manager.audience_lint(
            TEST_COURSE_ID, override_audience="id")
        self.assertEqual(result["violations"], [],
                         "id audience has no forbidden tokens")

    def test_invalid_override_audience_returns_error(self):
        result = mockup_manager.audience_lint(
            TEST_COURSE_ID, override_audience="bogus")
        self.assertFalse(result["ok"])
        self.assertIn("Invalid audience level", result["error"])


class TestWordBoundaryPattern(unittest.TestCase):
    r"""Verify \bv\d+\b does not false-positive on technical version strings."""

    def test_v3_matches(self):
        """Standalone v3 is a forbidden token."""
        import re
        pattern = re.compile(r"\bv\d+\b", re.IGNORECASE)
        self.assertIsNotNone(pattern.search("Proposal v3"))

    def test_v3_in_sentence(self):
        """v3 surrounded by spaces matches."""
        import re
        pattern = re.compile(r"\bv\d+\b", re.IGNORECASE)
        self.assertIsNotNone(pattern.search("this is v3 of the doc"))

    def test_cccv1p2_does_not_match(self):
        """cccv1p2-style technical strings should NOT match."""
        import re
        pattern = re.compile(r"\bv\d+\b", re.IGNORECASE)
        self.assertIsNone(pattern.search("cccv1p2"),
                          "cccv1p2 must not match \\bv\\d+\\b")

    def test_cccv1_does_not_match(self):
        """cccv1-style (no trailing non-digit separator) should NOT match."""
        import re
        pattern = re.compile(r"\bv\d+\b", re.IGNORECASE)
        self.assertIsNone(pattern.search("cccv1"),
                          "cccv1 must not match \\bv\\d+\\b")

    def test_v10_matches(self):
        """Multi-digit version (v10) matches."""
        import re
        pattern = re.compile(r"\bv\d+\b", re.IGNORECASE)
        self.assertIsNotNone(pattern.search("draft v10 submitted"))

    def test_canvas_api_url_path_version_segment_does_not_flag(self):
        url = "https://canvas.asu.edu/api/v1/courses/256173/files/132035535"
        for audience in ("faculty", "ta", "student"):
            with self.subTest(audience=audience):
                self.assertEqual(mockup_manager.lint_content(url, audience), [])

    def test_version_marker_text_still_flags_restricted_audiences(self):
        text = "Use v2 of the rubric and see v1 before publishing."
        for audience in ("faculty", "ta", "student"):
            with self.subTest(audience=audience):
                tokens = [
                    v["token"]
                    for v in mockup_manager.lint_content(text, audience)
                ]
                self.assertIn("v2", tokens)
                self.assertIn("v1", tokens)

    def test_published_instrument_cite_exclusions_still_pass_clean(self):
        text = (
            "Michie BCT Taxonomy v1, BCT v1, ICD-10 v1, and CCBC v1 "
            "are published instrument citations."
        )
        for audience in ("faculty", "student"):
            with self.subTest(audience=audience):
                self.assertEqual(mockup_manager.lint_content(text, audience), [])

    def test_bct_taxonomy_v1_does_not_match(self):
        """Legitimate published instrument cites like 'BCT Taxonomy v1' must
        NOT match the faculty version-marker pattern. The published taxonomy
        is canonically cited as 'Michie BCT Taxonomy v1' throughout the PA
        intervention literature; flagging it would produce noise on every
        rationale doc citing Michie et al."""
        from mockup_manager import _COMPILED_PATTERNS
        # Find the version-marker pattern in faculty's compiled list
        faculty_patterns = _COMPILED_PATTERNS["faculty"]
        version_pattern = next(
            (p for p in faculty_patterns if "v\\d" in p.pattern), None)
        self.assertIsNotNone(version_pattern,
                             "expected version pattern in faculty list")
        # The actual compiled pattern (with lookbehinds) must NOT match these:
        self.assertIsNone(
            version_pattern.search("Michie BCT Taxonomy v1 contains 93 BCTs"),
            "BCT Taxonomy v1 is a published instrument name; must not flag")
        self.assertIsNone(
            version_pattern.search("Use the BCT v1 reference card"),
            "BCT v1 reference card cite must not flag")
        # AND must STILL match the actual version markers we want to catch:
        self.assertIsNotNone(
            version_pattern.search("This is the redesign v3 proposal"),
            "redesign v3 must still flag as a version marker")


class TestSavePageAudienceKwarg(unittest.TestCase):
    """save_page() stores audience in .meta.json."""

    def setUp(self):
        import tempfile
        self._tmpdir = tempfile.mkdtemp(prefix="asucb_test_")
        self._patcher = patch.object(
            mockup_manager, "_mockup_dir",
            side_effect=lambda course_id, subdir=None: self._make_dir(
                course_id, subdir))
        self._patcher.start()

    def _make_dir(self, course_id, subdir=None):
        parts = [self._tmpdir, "courses", str(course_id), "mockup"]
        if subdir:
            parts.append(subdir)
        path = os.path.join(*parts)
        os.makedirs(path, exist_ok=True)
        return path

    def tearDown(self):
        self._patcher.stop()
        shutil.rmtree(self._tmpdir, ignore_errors=True)

    def test_audience_persisted_to_meta(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "00-rationale.html", "<p>Content</p>",
            audience="faculty",
        )
        pages_dir = mockup_manager._mockup_dir(TEST_COURSE_ID, "pages")
        meta_path = os.path.join(pages_dir, "00-rationale.html.meta.json")
        self.assertTrue(os.path.exists(meta_path))
        with open(meta_path, "r") as f:
            meta = json.load(f)
        self.assertEqual(meta.get("audience"), "faculty")

    def test_no_audience_kwarg_no_audience_in_meta(self):
        """When audience not supplied, meta should not have an audience key."""
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<p>Content</p>",
        )
        pages_dir = mockup_manager._mockup_dir(TEST_COURSE_ID, "pages")
        meta_path = os.path.join(pages_dir, "m1-overview.html.meta.json")
        with open(meta_path, "r") as f:
            meta = json.load(f)
        self.assertNotIn("audience", meta,
                         "audience key must not be written when kwarg is None")

    def test_invalid_audience_not_persisted(self):
        """Invalid audience values are silently dropped (not stored)."""
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<p>Content</p>",
            audience="invalid_level",
        )
        pages_dir = mockup_manager._mockup_dir(TEST_COURSE_ID, "pages")
        meta_path = os.path.join(pages_dir, "m1-overview.html.meta.json")
        with open(meta_path, "r") as f:
            meta = json.load(f)
        self.assertNotIn("audience", meta)


class TestGeneratePreviewLintWarning(unittest.TestCase):
    """generate_preview() emits lint warning when violations exist."""

    def setUp(self):
        import tempfile
        self._tmpdir = tempfile.mkdtemp(prefix="asucb_test_")
        self._patcher = patch.object(
            mockup_manager, "_mockup_dir",
            side_effect=lambda course_id, subdir=None: self._make_dir(
                course_id, subdir))
        self._patcher.start()

    def _make_dir(self, course_id, subdir=None):
        parts = [self._tmpdir, "courses", str(course_id), "mockup"]
        if subdir:
            parts.append(subdir)
        path = os.path.join(*parts)
        os.makedirs(path, exist_ok=True)
        return path

    def tearDown(self):
        self._patcher.stop()
        shutil.rmtree(self._tmpdir, ignore_errors=True)

    def test_warning_emitted_on_violations(self):
        """generate_preview should print WARNING to stderr when lint finds violations."""
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test Course")
        mockup_manager.save_page(
            TEST_COURSE_ID, "00-rationale.html",
            "<p>Proposal v3 using HPI/ID methodology</p>",
            audience="faculty",
        )
        import io
        stderr_capture = io.StringIO()
        with patch("sys.stderr", stderr_capture):
            result = mockup_manager.generate_preview(TEST_COURSE_ID)
        self.assertTrue(result["ok"])
        warning_output = stderr_capture.getvalue()
        self.assertIn("WARNING", warning_output)
        self.assertIn("audience-forbidden", warning_output)

    def test_no_warning_when_clean(self):
        """generate_preview should not emit WARNING when all pages are clean."""
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test Course")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html",
            "<h2>Clean Content</h2>",
            audience="student",
        )
        import io
        stderr_capture = io.StringIO()
        with patch("sys.stderr", stderr_capture):
            result = mockup_manager.generate_preview(TEST_COURSE_ID)
        self.assertTrue(result["ok"])
        warning_output = stderr_capture.getvalue()
        self.assertNotIn("WARNING", warning_output)


if __name__ == "__main__":
    unittest.main()
