"""Tests for mockup_manager.py — verify mockup CRUD, brief flow, and promote contracts.

Tests verify:
- manifest create/load/update lifecycle
- page save/load with page_type metadata
- save_page with brief_data auto-saves structured brief alongside placeholder
- brief save/load/list operations
- promote_page copies page-type HTML to staging, rejects placeholders
- promote_all stages pages, reports existing briefs, flags missing briefs
- generate_preview creates self-contained HTML with navigation
- list_mockups scans course directories
"""

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

import pytest

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

import mockup_manager
import staging_manager

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


@pytest.fixture(autouse=True)
def isolated_plugin_root(tmp_path, monkeypatch):
    """Keep mockup tests from writing through the courses/ symlink."""
    global PLUGIN_ROOT
    original_plugin_root = PLUGIN_ROOT
    PLUGIN_ROOT = str(tmp_path)
    monkeypatch.setattr(mockup_manager, "get_plugin_root", lambda: str(tmp_path))
    monkeypatch.setattr(staging_manager, "get_plugin_root", lambda: str(tmp_path))
    yield
    PLUGIN_ROOT = original_plugin_root


class TestManifest(unittest.TestCase):
    """Manifest CRUD operations."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_create_manifest(self):
        manifest = mockup_manager.create_manifest(
            TEST_COURSE_ID, "BIO 101", term="Fall 2027", faculty="Dr. Chen")
        self.assertEqual(manifest["courseId"], TEST_COURSE_ID)
        self.assertEqual(manifest["courseName"], "BIO 101")
        self.assertEqual(manifest["term"], "Fall 2027")
        self.assertEqual(manifest["faculty"], "Dr. Chen")
        self.assertEqual(manifest["version"], 1)
        self.assertEqual(manifest["status"], "pending")

    def test_load_manifest(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "BIO 101")
        loaded = mockup_manager.load_manifest(TEST_COURSE_ID)
        self.assertIsNotNone(loaded)
        self.assertEqual(loaded["courseName"], "BIO 101")

    def test_load_nonexistent_returns_none(self):
        loaded = mockup_manager.load_manifest("nonexistent")
        self.assertIsNone(loaded)

    def test_update_manifest(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "BIO 101")
        updated = mockup_manager.update_manifest(
            TEST_COURSE_ID, status="approved", faculty="Dr. Smith")
        self.assertEqual(updated["status"], "approved")
        self.assertEqual(updated["faculty"], "Dr. Smith")
        # Original fields preserved
        self.assertEqual(updated["courseName"], "BIO 101")


class TestPageCRUD(unittest.TestCase):
    """Page save, load, and list operations."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_save_and_load_page(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        result = mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")
        self.assertTrue(result["ok"])
        self.assertEqual(result["page_type"], "page")

        html = mockup_manager.load_page(TEST_COURSE_ID, "m1-overview.html")
        self.assertEqual(html, "<h2>Overview</h2>")

    def test_save_placeholder(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        result = mockup_manager.save_page(
            TEST_COURSE_ID, "m1-knowledge-check.html",
            "<div>quiz placeholder</div>", page_type="placeholder")
        self.assertEqual(result["page_type"], "placeholder")

    def test_list_pages_includes_type(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-knowledge-check.html", "<div>quiz</div>",
            page_type="placeholder")

        pages = mockup_manager.list_pages(TEST_COURSE_ID)
        self.assertEqual(len(pages), 2)
        types = {p["filename"]: p["page_type"] for p in pages}
        self.assertEqual(types["m1-overview.html"], "page")
        self.assertEqual(types["m1-knowledge-check.html"], "placeholder")

    def test_load_nonexistent_returns_none(self):
        html = mockup_manager.load_page(TEST_COURSE_ID, "nope.html")
        self.assertIsNone(html)

    def test_auto_appends_html_extension(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview", "<h2>Test</h2>")
        html = mockup_manager.load_page(TEST_COURSE_ID, "m1-overview")
        self.assertEqual(html, "<h2>Test</h2>")


class TestBriefAutoSave(unittest.TestCase):
    """save_page with brief_data auto-saves structured brief."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_brief_saved_with_placeholder(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        brief_data = {
            "questions": [
                {"question_text": "What is mitosis?",
                 "question_type": "multiple_choice_question",
                 "answers": [
                     {"answer_text": "Cell division", "answer_weight": 100},
                     {"answer_text": "Cell death", "answer_weight": 0},
                 ],
                 "points_possible": 3,
                 "correct_comments": "Correct!",
                 "incorrect_comments": "Review chapter 4."}
            ],
            "settings": {"time_limit": None, "allowed_attempts": -1}
        }
        result = mockup_manager.save_page(
            TEST_COURSE_ID, "m1-knowledge-check.html",
            "<div>quiz placeholder</div>",
            page_type="placeholder", brief_data=brief_data)

        self.assertTrue(result["ok"])
        self.assertIn("brief_path", result)

        # Verify brief is loadable with full content
        brief = mockup_manager.load_brief(TEST_COURSE_ID, "knowledge-check", 1)
        self.assertIsNotNone(brief)
        self.assertEqual(len(brief["questions"]), 1)
        self.assertEqual(brief["questions"][0]["question_text"],
                         "What is mitosis?")
        self.assertEqual(brief["questions"][0]["answers"][0]["answer_weight"],
                         100)

    def test_discussion_brief_saved(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        brief_data = {
            "prompt": "Compare and contrast mitosis and meiosis.",
            "discussion_type": "threaded",
            "grading": {"points_possible": 25},
            "replies_required": 2,
        }
        result = mockup_manager.save_page(
            TEST_COURSE_ID, "m2-discussion.html",
            "<div>discussion placeholder</div>",
            page_type="placeholder", brief_data=brief_data)

        brief = mockup_manager.load_brief(TEST_COURSE_ID, "discussion", 2)
        self.assertIsNotNone(brief)
        self.assertEqual(brief["prompt"],
                         "Compare and contrast mitosis and meiosis.")
        self.assertEqual(brief["grading"]["points_possible"], 25)

    def test_assignment_brief_saved(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        brief_data = {
            "description": "Create a lab report analyzing cell samples.",
            "rubric": [
                {"criterion": "Analysis",
                 "ratings": [{"description": "Excellent", "points": 10},
                             {"description": "Adequate", "points": 7}]},
            ],
            "submission_types": ["online_upload"],
            "points": 30,
        }
        result = mockup_manager.save_page(
            TEST_COURSE_ID, "m1-assignment.html",
            "<div>assignment placeholder</div>",
            page_type="placeholder", brief_data=brief_data)

        brief = mockup_manager.load_brief(TEST_COURSE_ID, "assignment", 1)
        self.assertIsNotNone(brief)
        self.assertEqual(brief["description"],
                         "Create a lab report analyzing cell samples.")
        self.assertEqual(len(brief["rubric"]), 1)
        self.assertEqual(brief["rubric"][0]["criterion"], "Analysis")

    def test_lti_tool_brief_saved(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        brief_data = {
            "type": "lti_tool",
            "tool_name": "Harmonize",
            "module_number": 3,
            "title": "Module 3 Discussion Forum",
            "purpose": "Community discussion for module concepts",
            "configuration_notes": "Teacher-configured forum with groups.",
            "integration_requirements": [],
        }
        result = mockup_manager.save_page(
            TEST_COURSE_ID, "m3-harmonize.html",
            "<div>tool placeholder</div>",
            page_type="placeholder", brief_data=brief_data)

        self.assertTrue(result["ok"])
        self.assertIn("brief_path", result)

        brief = mockup_manager.load_brief(TEST_COURSE_ID, "lti_tool", 3)
        self.assertIsNotNone(brief)
        self.assertEqual(brief["tool_name"], "Harmonize")
        self.assertEqual(brief["title"], "Module 3 Discussion Forum")

    def test_no_brief_for_page_type(self):
        """page-type saves should not create briefs."""
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        result = mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>",
            page_type="page", brief_data={"should": "be ignored"})
        self.assertNotIn("brief_path", result)

    def test_no_brief_without_brief_data(self):
        """Placeholder without brief_data should not create brief."""
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-knowledge-check.html",
            "<div>quiz</div>", page_type="placeholder")
        brief = mockup_manager.load_brief(
            TEST_COURSE_ID, "knowledge-check", 1)
        self.assertIsNone(brief)


class TestBriefCRUD(unittest.TestCase):
    """Direct brief save/load/list operations."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_save_and_load_brief(self):
        result = mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1,
            {"questions": [{"q": "test"}]})
        self.assertTrue(result["ok"])

        brief = mockup_manager.load_brief(
            TEST_COURSE_ID, "knowledge-check", 1)
        self.assertEqual(brief["questions"][0]["q"], "test")

    def test_invalid_brief_type(self):
        result = mockup_manager.save_brief(
            TEST_COURSE_ID, "invalid-type", 1, {})
        self.assertFalse(result["ok"])

    def test_list_briefs(self):
        mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1, {"q": []})
        mockup_manager.save_brief(
            TEST_COURSE_ID, "discussion", 2, {"p": ""})
        mockup_manager.save_brief(
            TEST_COURSE_ID, "lti_tool", 3, {"type": "lti_tool"})

        briefs = mockup_manager.list_briefs(TEST_COURSE_ID)
        self.assertEqual(len(briefs), 3)
        types = {b["brief_type"] for b in briefs}
        self.assertEqual(types, {"knowledge-check", "discussion", "lti_tool"})

    def test_same_type_module_different_slug_briefs_persist(self):
        first = mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1,
            {"questions": [{"q": "intro"}]}, slug="intro-check")
        second = mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1,
            {"questions": [{"q": "review"}]}, slug="review-check")

        self.assertTrue(first["ok"])
        self.assertTrue(second["ok"])
        self.assertTrue(os.path.exists(first["path"]))
        self.assertTrue(os.path.exists(second["path"]))
        self.assertNotEqual(first["path"], second["path"])

    def test_same_slug_different_content_triggers_collision_guard(self):
        first = mockup_manager.save_brief(
            TEST_COURSE_ID, "discussion", 2,
            {"prompt": "First prompt"}, slug="weekly-prompt")
        second = mockup_manager.save_brief(
            TEST_COURSE_ID, "discussion", 2,
            {"prompt": "Changed prompt"}, slug="weekly-prompt")

        self.assertTrue(first["ok"])
        self.assertFalse(second["ok"])
        self.assertEqual(
            second["error"],
            "Brief collision: path already exists with different content. "
            "Pass a distinct slug.",
        )

    def test_same_slug_same_content_is_idempotent(self):
        content = {"prompt": "Same prompt", "points": 10}
        first = mockup_manager.save_brief(
            TEST_COURSE_ID, "assignment", 1, content, slug="lab-report")
        second = mockup_manager.save_brief(
            TEST_COURSE_ID, "assignment", 1, content, slug="lab-report")

        self.assertTrue(first["ok"])
        self.assertTrue(second["ok"])
        self.assertEqual(first["path"], second["path"])

    def test_load_brief_with_slug_returns_exact_brief(self):
        mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1,
            {"questions": [{"q": "intro"}]}, slug="intro-check")
        mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1,
            {"questions": [{"q": "review"}]}, slug="review-check")

        brief = mockup_manager.load_brief(
            TEST_COURSE_ID, "knowledge-check", 1, slug="review-check")
        self.assertEqual(brief["questions"][0]["q"], "review")

    def test_load_brief_without_slug_with_two_matches_returns_none(self):
        mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1,
            {"questions": [{"q": "intro"}]}, slug="intro-check")
        mockup_manager.save_brief(
            TEST_COURSE_ID, "knowledge-check", 1,
            {"questions": [{"q": "review"}]}, slug="review-check")

        with self.assertLogs(level="WARNING") as logs:
            brief = mockup_manager.load_brief(
                TEST_COURSE_ID, "knowledge-check", 1)
        self.assertIsNone(brief)
        self.assertIn("Multiple briefs match knowledge-check module 1", logs.output[0])

    def test_load_brief_without_slug_with_one_match_returns_it(self):
        mockup_manager.save_brief(
            TEST_COURSE_ID, "assignment", 1,
            {"title": "Only Assignment", "points": 10})

        brief = mockup_manager.load_brief(TEST_COURSE_ID, "assignment", 1)
        self.assertEqual(brief["title"], "Only Assignment")

    def test_list_briefs_includes_new_and_old_shapes(self):
        briefs_dir = mockup_manager._mockup_dir(TEST_COURSE_ID, "briefs")
        old_path = os.path.join(briefs_dir, "discussion-m2.json")
        with open(old_path, "w", encoding="utf-8") as f:
            json.dump({"prompt": "legacy"}, f)
        mockup_manager.save_brief(
            TEST_COURSE_ID, "discussion", 2,
            {"prompt": "modern"}, slug="modern")

        briefs = mockup_manager.list_briefs(TEST_COURSE_ID)
        by_filename = {b["filename"]: b for b in briefs}
        self.assertEqual(by_filename["discussion-m2.json"]["slug"], "")
        self.assertEqual(by_filename["discussion-m2-modern.json"]["slug"], "modern")

    def test_slug_derives_from_title_with_special_chars(self):
        result = mockup_manager.save_brief(
            TEST_COURSE_ID, "assignment", 2,
            {"title": "Lab #1: Hypothesis & Testing!!!", "points": 5})

        self.assertTrue(result["ok"])
        self.assertEqual(result["slug"], "lab-1-hypothesis-testing")
        self.assertEqual(
            os.path.basename(result["path"]),
            "assignment-m2-lab-1-hypothesis-testing.json",
        )


class TestPromotePage(unittest.TestCase):
    """promote_page copies page HTML to staging, rejects placeholders."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_promotes_page_to_staging(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")

        result = mockup_manager.promote_page(
            TEST_COURSE_ID, "m1-overview.html")
        self.assertTrue(result["ok"])
        self.assertEqual(result["slug"], "m1-overview")

        # Verify content is in staging
        staged = staging_manager.get_staged(
            TEST_COURSE_ID, "pages", "m1-overview")
        self.assertEqual(staged, "<h2>Overview</h2>")

    def test_rejects_placeholder(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-knowledge-check.html", "<div>quiz</div>",
            page_type="placeholder")

        result = mockup_manager.promote_page(
            TEST_COURSE_ID, "m1-knowledge-check.html")
        self.assertFalse(result["ok"])
        self.assertIn("placeholder", result["error"])

    def test_nonexistent_page(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        result = mockup_manager.promote_page(TEST_COURSE_ID, "nope.html")
        self.assertFalse(result["ok"])


class TestPromoteAll(unittest.TestCase):
    """promote_all stages pages and reports brief availability."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_promotes_pages_and_finds_briefs(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-knowledge-check.html", "<div>quiz</div>",
            page_type="placeholder",
            brief_data={"questions": [{"q": "What?"}]})

        result = mockup_manager.promote_all(TEST_COURSE_ID)
        self.assertTrue(result["ok"])
        self.assertEqual(result["promoted"], ["m1-overview.html"])
        self.assertEqual(len(result["briefs_available"]), 1)
        self.assertEqual(result["briefs_available"][0]["brief_type"],
                         "knowledge-check")
        self.assertEqual(len(result["needs_attention"]), 0)

    def test_flags_placeholder_without_brief(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        # Placeholder saved WITHOUT brief_data
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-discussion.html", "<div>disc</div>",
            page_type="placeholder")

        result = mockup_manager.promote_all(TEST_COURSE_ID)
        self.assertEqual(len(result["needs_attention"]), 1)
        self.assertIn("no brief was saved",
                       result["needs_attention"][0]["issue"])

    def test_flags_external_tool_placeholder(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        # External tool doesn't match brief type pattern
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-harmonize.html", "<div>tool</div>",
            page_type="placeholder")

        result = mockup_manager.promote_all(TEST_COURSE_ID)
        self.assertEqual(len(result["needs_attention"]), 1)
        self.assertIn("External tool",
                       result["needs_attention"][0]["issue"])

    def test_promote_all_finds_lti_tool_brief(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m3-harmonize.html", "<div>tool</div>",
            page_type="placeholder",
            brief_data={
                "type": "lti_tool",
                "tool_name": "Harmonize",
                "module_number": 3,
                "title": "Module 3 Discussion Forum",
                "purpose": "Community discussion for module concepts",
                "configuration_notes": "Teacher-configured forum with groups.",
                "integration_requirements": [],
            })

        result = mockup_manager.promote_all(TEST_COURSE_ID)
        self.assertEqual(len(result["briefs_available"]), 1)
        self.assertEqual(result["briefs_available"][0]["brief_type"],
                         "lti_tool")
        self.assertEqual(len(result["needs_attention"]), 0)

    def test_promote_all_emits_mockup_promoted_event(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m3-harmonize.html", "<div>tool</div>",
            page_type="placeholder",
            brief_data={
                "type": "lti_tool",
                "tool_name": "Harmonize",
                "module_number": 3,
                "title": "Module 3 Discussion Forum",
                "purpose": "Community discussion for module concepts",
                "configuration_notes": "Teacher-configured forum with groups.",
                "integration_requirements": [],
            })

        with patch.object(mockup_manager, "_emit_mockup_event") as emit_event:
            result = mockup_manager.promote_all(TEST_COURSE_ID)

        self.assertTrue(result["ok"])
        emit_event.assert_called_once_with(TEST_COURSE_ID, "mockup_promoted", {
            "pages_promoted": 1,
            "briefs_promoted": 1,
            "subheaders_skipped": 0,
            "staging_paths": [
                os.path.join(
                    PLUGIN_ROOT, "courses", TEST_COURSE_ID,
                    "staging", "pages", "m1-overview.html"),
            ],
        })


class TestSubHeader(unittest.TestCase):
    """SubHeader page_type for Canvas module dividers (text-only items)."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_save_page_subheader_writes_sidecar(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test", "")
        result = mockup_manager.save_page(
            TEST_COURSE_ID,
            "m1-00a-overview-section.html",
            "<h3>Overview</h3>",
            page_type="subheader",
        )
        self.assertTrue(result["ok"])
        self.assertEqual(result["page_type"], "subheader")
        self.assertTrue(os.path.exists(result["path"]))
        with open(result["path"], "r", encoding="utf-8") as f:
            html = f.read()
        self.assertEqual(
            html, '<h3 class="module-subheader">Overview</h3>')

        meta_path = result["path"] + ".meta.json"
        self.assertTrue(os.path.exists(meta_path))
        with open(meta_path, "r", encoding="utf-8") as f:
            meta = json.load(f)
        self.assertEqual(meta["page_type"], "subheader")
        self.assertEqual(meta["subheader_text"], "Overview")

    def test_list_pages_returns_subheader_metadata(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test", "")
        mockup_manager.save_page(
            TEST_COURSE_ID,
            "m1-00a-overview-section.html",
            "<h3>Overview</h3>",
            page_type="subheader",
        )
        pages = mockup_manager.list_pages(TEST_COURSE_ID)
        sub = [p for p in pages if p["page_type"] == "subheader"]
        self.assertEqual(len(sub), 1)
        self.assertEqual(sub[0]["subheader_text"], "Overview")

    def test_promote_subheader_skips_with_reason(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test", "")
        mockup_manager.save_page(
            TEST_COURSE_ID,
            "m1-00a-overview-section.html",
            "<h3>Overview</h3>",
            page_type="subheader",
        )
        result = mockup_manager.promote_page(
            TEST_COURSE_ID, "m1-00a-overview-section.html")
        self.assertEqual(result, {
            "ok": True,
            "skipped": True,
            "reason": "subheader",
        })

        # Verify nothing was actually written to staging
        staging_path = os.path.join(
            PLUGIN_ROOT, "courses", TEST_COURSE_ID,
            "staging", "pages", "m1-00a-overview-section.html")
        self.assertFalse(os.path.exists(staging_path))

    def test_classify_page_returns_subheader_color(self):
        color, label = mockup_manager._classify_page("m1-00a-overview-section")
        self.assertEqual(color, "#94a3b8")
        self.assertEqual(label, "SubHeader")

    def test_promote_all_skips_subheaders(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test", "")
        mockup_manager.save_page(
            TEST_COURSE_ID,
            "m1-00a-overview-section.html",
            "<h3>Overview</h3>",
            page_type="subheader",
        )
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")

        result = mockup_manager.promote_all(TEST_COURSE_ID)
        self.assertTrue(result["ok"])
        self.assertEqual(result["promoted"], ["m1-overview.html"])
        self.assertEqual(result["subheaders_skipped"], 1)

        staging_path = os.path.join(
            PLUGIN_ROOT, "courses", TEST_COURSE_ID,
            "staging", "pages", "m1-00a-overview-section.html")
        self.assertFalse(os.path.exists(staging_path))


class TestGeneratePreview(unittest.TestCase):
    """Preview generation creates self-contained HTML."""

    def tearDown(self):
        course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)
        if os.path.exists(course_dir):
            shutil.rmtree(course_dir)

    def test_generates_preview(self):
        mockup_manager.create_manifest(
            TEST_COURSE_ID, "BIO 101", term="Fall 2027")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html",
            "<h2>Module 1 Overview</h2>")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-lesson.html",
            "<h2>Module 1 Lesson</h2>")

        result = mockup_manager.generate_preview(TEST_COURSE_ID)
        self.assertTrue(result["ok"])
        self.assertEqual(result["page_count"], 2)
        self.assertEqual(
            os.path.basename(result["path"]), "BIO 101 Mockup.html")

        # Read the preview file
        with open(result["path"], "r") as f:
            html = f.read()
        self.assertIn("BIO 101", html)
        self.assertIn("m1-overview", html)
        self.assertIn("m1-lesson", html)
        self.assertIn("Module 1 Overview", html)

    def test_preview_versioning(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>v1</h2>")

        # Generate first preview
        r1 = mockup_manager.generate_preview(TEST_COURSE_ID)
        self.assertEqual(r1["version"], 1)

        # Generate second — should version the first
        r2 = mockup_manager.generate_preview(TEST_COURSE_ID)
        self.assertEqual(r2["version"], 2)

        # Old version should exist
        v1_path = os.path.join(
            mockup_manager._mockup_dir(TEST_COURSE_ID), "Test Mockup-v1.html")
        self.assertTrue(os.path.exists(v1_path))

    def test_preview_filename_uses_sanitized_course_name(self):
        mockup_manager.create_manifest(
            TEST_COURSE_ID, "BIO 101: Cell Biology")
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")

        result = mockup_manager.generate_preview(TEST_COURSE_ID)

        self.assertTrue(result["ok"])
        self.assertEqual(
            os.path.basename(result["path"]),
            "BIO 101 Cell Biology Mockup.html")
        self.assertTrue(os.path.exists(result["path"]))

    def test_preview_filename_falls_back_without_course_name(self):
        manifest = mockup_manager.create_manifest(TEST_COURSE_ID, "BIO 101")
        manifest.pop("courseName")
        with open(mockup_manager._manifest_path(TEST_COURSE_ID), "w",
                  encoding="utf-8") as f:
            json.dump(manifest, f)
        mockup_manager.save_page(
            TEST_COURSE_ID, "m1-overview.html", "<h2>Overview</h2>")

        result = mockup_manager.generate_preview(TEST_COURSE_ID)

        self.assertTrue(result["ok"])
        self.assertEqual(
            os.path.basename(result["path"]),
            f"Course {TEST_COURSE_ID} Mockup.html")
        self.assertTrue(os.path.exists(result["path"]))

    def test_no_pages_returns_error(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Test")
        result = mockup_manager.generate_preview(TEST_COURSE_ID)
        self.assertFalse(result["ok"])

    def test_no_manifest_returns_error(self):
        result = mockup_manager.generate_preview("nonexistent")
        self.assertFalse(result["ok"])


class TestListMockups(unittest.TestCase):
    """list_mockups scans course directories."""

    def tearDown(self):
        for cid in [TEST_COURSE_ID, "99996"]:
            course_dir = os.path.join(PLUGIN_ROOT, "courses", cid)
            if os.path.exists(course_dir):
                shutil.rmtree(course_dir)

    def test_finds_mockups(self):
        mockup_manager.create_manifest(TEST_COURSE_ID, "Course A")
        mockup_manager.create_manifest("99996", "Course B")

        mockups = mockup_manager.list_mockups()
        ids = {m["courseId"] for m in mockups}
        self.assertIn(TEST_COURSE_ID, ids)
        self.assertIn("99996", ids)

    def test_empty_when_no_mockups(self):
        mockups = mockup_manager.list_mockups()
        # May find mockups from other tests but should not crash
        self.assertIsInstance(mockups, list)


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