"""Extended tests for staging_manager.py — preflight, sidecars, meta.json.

Tests the Codex-added features:
- validate parameter triggers Tier 1 preflight
- .meta.json sidecar written when module_number/title provided
- list_staged skips sidecar files
- clear_staged removes sidecars
- SIDECAR_SUFFIXES filtering
"""

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

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

import staging_manager

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


class TestMetaJsonSidecar(unittest.TestCase):
    """Test .meta.json sidecar writing for module grouping."""

    def setUp(self):
        self.course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)

    def tearDown(self):
        if os.path.exists(self.course_dir):
            shutil.rmtree(self.course_dir)

    def test_meta_written_when_module_number_provided(self):
        result = staging_manager.stage(
            TEST_COURSE_ID, "assignments", "12345",
            "<p>Instructions</p>", validate=False,
            module_number=3, title="Module 3 Assignment"
        )
        self.assertTrue(result["ok"])
        meta_path = result["path"] + ".meta.json"
        self.assertTrue(os.path.exists(meta_path))
        with open(meta_path) as f:
            meta = json.load(f)
        self.assertEqual(meta["module_number"], 3)
        self.assertEqual(meta["title"], "Module 3 Assignment")

    def test_no_meta_when_params_absent(self):
        result = staging_manager.stage(
            TEST_COURSE_ID, "pages", "m1-overview",
            "<h2>Overview</h2>", validate=False
        )
        meta_path = result["path"] + ".meta.json"
        self.assertFalse(os.path.exists(meta_path))

    def test_meta_with_only_module_number(self):
        result = staging_manager.stage(
            TEST_COURSE_ID, "discussions", "67890",
            "<p>Discuss</p>", validate=False,
            module_number=5
        )
        meta_path = result["path"] + ".meta.json"
        self.assertTrue(os.path.exists(meta_path))
        with open(meta_path) as f:
            meta = json.load(f)
        self.assertEqual(meta["module_number"], 5)
        self.assertNotIn("title", meta)


class TestSidecarFiltering(unittest.TestCase):
    """Test that list_staged skips sidecar files."""

    def setUp(self):
        self.course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)

    def tearDown(self):
        if os.path.exists(self.course_dir):
            shutil.rmtree(self.course_dir)

    def test_list_skips_issues_json(self):
        # Stage a page
        staging_manager.stage(
            TEST_COURSE_ID, "pages", "m1-overview",
            "<h2>Test</h2>", validate=False
        )
        # Manually create sidecar files that might confuse listing
        staging_dir = os.path.join(
            self.course_dir, "staging", "pages"
        )
        # Create .issues.json sidecar
        with open(os.path.join(staging_dir, "m1-overview.html.issues.json"), "w") as f:
            json.dump({"total": 0}, f)
        # Create .meta.json sidecar
        with open(os.path.join(staging_dir, "m1-overview.html.meta.json"), "w") as f:
            json.dump({"module_number": 1}, f)

        items = staging_manager.list_staged(TEST_COURSE_ID, "pages")
        # Should only find the actual page, not sidecars
        self.assertEqual(len(items), 1)
        self.assertEqual(items[0]["object_id"], "m1-overview")

    def test_list_skips_quiz_sidecar(self):
        # Stage a quiz
        staging_manager.stage(
            TEST_COURSE_ID, "quizzes", "555",
            {"quiz": {"title": "Test"}}, validate=False
        )
        # Create sidecar that also ends in .json
        staging_dir = os.path.join(
            self.course_dir, "staging", "quizzes"
        )
        with open(os.path.join(staging_dir, "555.json.issues.json"), "w") as f:
            json.dump({"total": 2}, f)

        items = staging_manager.list_staged(TEST_COURSE_ID, "quizzes")
        self.assertEqual(len(items), 1)
        self.assertEqual(items[0]["object_id"], "555")


class TestClearSidecars(unittest.TestCase):
    """Test that clear_staged removes sidecar files."""

    def setUp(self):
        self.course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)

    def tearDown(self):
        if os.path.exists(self.course_dir):
            shutil.rmtree(self.course_dir)

    def test_clear_removes_all_sidecars(self):
        result = staging_manager.stage(
            TEST_COURSE_ID, "pages", "m2-lesson",
            "<h2>Lesson</h2>", validate=False,
            module_number=2, title="Lesson"
        )
        main_path = result["path"]
        staging_dir = os.path.dirname(main_path)

        # Create all sidecar types
        for suffix in staging_manager.SIDECAR_SUFFIXES:
            with open(main_path + suffix, "w") as f:
                f.write("{}")

        # Verify they exist
        for suffix in staging_manager.SIDECAR_SUFFIXES:
            self.assertTrue(os.path.exists(main_path + suffix))

        # Clear
        staging_manager.clear_staged(TEST_COURSE_ID, "pages", "m2-lesson")

        # Main file gone
        self.assertFalse(os.path.exists(main_path))
        # All sidecars gone
        for suffix in staging_manager.SIDECAR_SUFFIXES:
            self.assertFalse(os.path.exists(main_path + suffix),
                             f"Sidecar {suffix} not cleaned up")

    def test_clear_all_removes_sidecars(self):
        staging_manager.stage(
            TEST_COURSE_ID, "pages", "p1",
            "<p>1</p>", validate=False
        )
        staging_manager.stage(
            TEST_COURSE_ID, "pages", "p2",
            "<p>2</p>", validate=False
        )
        # Create sidecars for p1
        p1_path = staging_manager._staged_path(TEST_COURSE_ID, "pages", "p1")
        with open(p1_path + ".issues.json", "w") as f:
            f.write("{}")

        result = staging_manager.clear_staged(TEST_COURSE_ID, "pages")
        self.assertTrue(result["ok"])
        # Both pages + sidecar should be gone
        self.assertEqual(len(staging_manager.list_staged(TEST_COURSE_ID, "pages")), 0)
        self.assertFalse(os.path.exists(p1_path + ".issues.json"))


class TestClearPushedStaged(unittest.TestCase):
    """Test cleanup of staged items already verified in timeline.jsonl."""

    def setUp(self):
        self.course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)

    def tearDown(self):
        if os.path.exists(self.course_dir):
            shutil.rmtree(self.course_dir)

    def _append_timeline(self, records):
        os.makedirs(self.course_dir, exist_ok=True)
        timeline_path = os.path.join(self.course_dir, "timeline.jsonl")
        with open(timeline_path, "w", encoding="utf-8") as f:
            for record in records:
                f.write(json.dumps(record) + "\n")

    def test_clear_pushed_staged_removes_only_verified_items(self):
        staging_manager.stage(
            TEST_COURSE_ID, "assignments", "12345",
            "<p>Instructions</p>", validate=False)
        staging_manager.stage(
            TEST_COURSE_ID, "quizzes", "555",
            {"quiz": {"title": "Quiz"}}, validate=False)
        staging_manager.stage(
            TEST_COURSE_ID, "pages", "m1-overview",
            "<h2>Overview</h2>", validate=False)
        self._append_timeline([
            {
                "event_type": "push_verified",
                "payload": {"object_type": "assignment", "object_id": "12345"},
            },
            {
                "event_type": "push_verified",
                "payload": {"object_type": "quiz", "object_id": "555"},
            },
        ])

        result = staging_manager.clear_pushed_staged(TEST_COURSE_ID)

        self.assertTrue(result["ok"])
        self.assertEqual(len(result["cleared_items"]), 2)
        self.assertEqual(len(result["skipped_items"]), 1)
        self.assertIsNone(
            staging_manager.get_staged(TEST_COURSE_ID, "assignments", "12345"))
        self.assertIsNone(
            staging_manager.get_staged(TEST_COURSE_ID, "quizzes", "555"))
        self.assertIsNotNone(
            staging_manager.get_staged(TEST_COURSE_ID, "pages", "m1-overview"))

    def test_clear_pushed_staged_respects_type_filter(self):
        staging_manager.stage(
            TEST_COURSE_ID, "assignments", "12345",
            "<p>Instructions</p>", validate=False)
        staging_manager.stage(
            TEST_COURSE_ID, "quizzes", "555",
            {"quiz": {"title": "Quiz"}}, validate=False)
        self._append_timeline([
            {
                "event_type": "push_verified",
                "payload": {"object_type": "assignment", "object_id": "12345"},
            },
            {
                "event_type": "push_verified",
                "payload": {"object_type": "quiz", "object_id": "555"},
            },
        ])

        result = staging_manager.clear_pushed_staged(
            TEST_COURSE_ID, "assignments")

        self.assertEqual(len(result["cleared_items"]), 1)
        self.assertIsNone(
            staging_manager.get_staged(TEST_COURSE_ID, "assignments", "12345"))
        self.assertIsNotNone(
            staging_manager.get_staged(TEST_COURSE_ID, "quizzes", "555"))


class TestPreflightIntegration(unittest.TestCase):
    """Test that validate=True triggers preflight and writes sidecar."""

    def setUp(self):
        self.course_dir = os.path.join(PLUGIN_ROOT, "courses", TEST_COURSE_ID)

    def tearDown(self):
        if os.path.exists(self.course_dir):
            shutil.rmtree(self.course_dir)

    @patch("staging_manager._run_tier1_preflight")
    def test_validate_true_calls_preflight(self, mock_preflight):
        mock_preflight.return_value = {"total": 0, "errors": 0, "warnings": 0}
        result = staging_manager.stage(
            TEST_COURSE_ID, "pages", "test-page",
            "<h2>Test</h2>", validate=True
        )
        mock_preflight.assert_called_once()
        self.assertIn("preflight", result)

    @patch("staging_manager._run_tier1_preflight")
    def test_validate_false_skips_preflight(self, mock_preflight):
        result = staging_manager.stage(
            TEST_COURSE_ID, "pages", "test-page",
            "<h2>Test</h2>", validate=False
        )
        mock_preflight.assert_not_called()
        self.assertNotIn("preflight", result)

    def test_preflight_graceful_when_module_missing(self):
        """If preflight_checks.py import fails, staging still works."""
        # _run_tier1_preflight catches ImportError gracefully
        # We test this by calling it directly (it may or may not import)
        result = staging_manager.stage(
            TEST_COURSE_ID, "pages", "test-page",
            "<h2>Test</h2><img src='x'>", validate=True
        )
        # Should succeed regardless of whether preflight_checks.py is importable
        self.assertTrue(result["ok"])


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