"""Tests for progress_tracker.py — verify build progress tracking contracts.

Tests verify:
- update_progress creates and updates build-progress.json correctly
- module status auto-calculates from page statuses
- slug is stored and available for drift detection
- history entries append correctly
- get_summary returns meaningful text
- try_parse_slug handles canonical and suffixed slugs
- check_drift uses stored slug first, falls back to canonical patterns
"""

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 progress_tracker

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


class TestUpdateProgress(unittest.TestCase):
    """Core update_progress functionality."""

    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_creates_progress_file(self):
        result = progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete")
        self.assertTrue(result["ok"])
        path = progress_tracker._progress_path(TEST_COURSE_ID)
        self.assertTrue(os.path.exists(path))

    def test_updates_page_status(self):
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        page = progress["modules"]["1"]["pages"]["overview"]
        self.assertEqual(page["status"], "complete")
        self.assertNotEqual(page["updatedAt"], "")

    def test_stores_note(self):
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "lesson", "in-progress", note="needs images")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        page = progress["modules"]["1"]["pages"]["lesson"]
        self.assertEqual(page["note"], "needs images")

    def test_stores_slug(self):
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "lesson", "complete",
            slug="m1-lesson-cellular-biology")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        page = progress["modules"]["1"]["pages"]["lesson"]
        self.assertEqual(page["slug"], "m1-lesson-cellular-biology")

    def test_rejects_invalid_page_type(self):
        result = progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "invalid-type", "complete")
        self.assertFalse(result["ok"])
        self.assertIn("Invalid page type", result["error"])

    def test_rejects_invalid_status(self):
        result = progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "invalid-status")
        self.assertFalse(result["ok"])
        self.assertIn("Invalid status", result["error"])


class TestModuleStatusCalculation(unittest.TestCase):
    """Module status derives from page statuses."""

    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_all_pending_gives_pending(self):
        # Creating module with one page pending leaves it pending
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "pending")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(progress["modules"]["1"]["status"], "pending")

    def test_any_in_progress_gives_in_progress(self):
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete")
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "lesson", "in-progress")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(progress["modules"]["1"]["status"], "in-progress")

    def test_any_complete_with_pending_gives_in_progress(self):
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete")
        # All others still pending
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(progress["modules"]["1"]["status"], "in-progress")

    def test_all_complete_gives_complete(self):
        for pt in progress_tracker.PAGE_TYPES:
            progress_tracker.update_progress(
                TEST_COURSE_ID, 1, pt, "complete")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(progress["modules"]["1"]["status"], "complete")


class TestHistory(unittest.TestCase):
    """History entries append with timestamps."""

    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_add_history_entry(self):
        result = progress_tracker.add_history_entry(
            TEST_COURSE_ID, "built", "modules 1-3")
        self.assertTrue(result["ok"])
        self.assertEqual(result["entry"]["action"], "built")
        self.assertEqual(result["entry"]["scope"], "modules 1-3")
        self.assertIn("date", result["entry"])

    def test_history_with_reason(self):
        progress_tracker.add_history_entry(
            TEST_COURSE_ID, "revised", "module 2 lesson",
            reason="faculty feedback")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        entry = progress["history"][0]
        self.assertEqual(entry["reason"], "faculty feedback")

    def test_multiple_entries_append(self):
        progress_tracker.add_history_entry(
            TEST_COURSE_ID, "built", "module 1")
        progress_tracker.add_history_entry(
            TEST_COURSE_ID, "built", "module 2")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(len(progress["history"]), 2)


class TestGetSummary(unittest.TestCase):
    """Summary returns meaningful text for dashboard display."""

    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_empty_returns_not_started(self):
        summary = progress_tracker.get_summary(TEST_COURSE_ID)
        self.assertEqual(summary, "Not started")

    def test_all_complete_returns_build_complete(self):
        for pt in progress_tracker.PAGE_TYPES:
            progress_tracker.update_progress(
                TEST_COURSE_ID, 1, pt, "complete")
        summary = progress_tracker.get_summary(TEST_COURSE_ID)
        self.assertIn("Build complete", summary)

    def test_in_progress_shows_current_work(self):
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete")
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "lesson", "in-progress", note="needs images")
        summary = progress_tracker.get_summary(TEST_COURSE_ID)
        self.assertIn("Module 1", summary)
        self.assertIn("lesson", summary)
        self.assertIn("needs images", summary)

    def test_in_progress_prioritized_over_pending(self):
        """In-progress pages should show before pending ones."""
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete")
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "lesson", "in-progress", note="editing")
        # prepare-to-learn is still pending but lesson is in-progress
        summary = progress_tracker.get_summary(TEST_COURSE_ID)
        self.assertIn("lesson", summary)
        self.assertNotIn("prepare to learn", summary)


class TestSlugParser(unittest.TestCase):
    """try_parse_slug handles various slug formats."""

    def test_canonical_slug(self):
        m, pt = progress_tracker.try_parse_slug("m1-overview")
        self.assertEqual(m, "1")
        self.assertEqual(pt, "overview")

    def test_suffixed_slug(self):
        m, pt = progress_tracker.try_parse_slug("m3-lesson-cellular-biology")
        self.assertEqual(m, "3")
        self.assertEqual(pt, "lesson")

    def test_hyphenated_page_type(self):
        m, pt = progress_tracker.try_parse_slug("m2-knowledge-check")
        self.assertEqual(m, "2")
        self.assertEqual(pt, "knowledge-check")

    def test_module_prefix(self):
        m, pt = progress_tracker.try_parse_slug("module-5-create-artifact")
        self.assertEqual(m, "5")
        self.assertEqual(pt, "create-artifact")

    def test_unrecognized_returns_none(self):
        m, pt = progress_tracker.try_parse_slug("some-random-page")
        self.assertIsNone(m)
        self.assertIsNone(pt)

    def test_numeric_id_returns_none(self):
        m, pt = progress_tracker.try_parse_slug("12345")
        self.assertIsNone(m)
        self.assertIsNone(pt)


class TestCheckDrift(unittest.TestCase):
    """Drift detection uses stored slug and falls back to canonical."""

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

    @patch("canvas_api.get_page")
    def test_uses_stored_slug(self, mock_get_page):
        """Should query Canvas with the stored slug, not just canonical."""
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "lesson", "complete",
            slug="m1-lesson-cellular-biology")

        mock_get_page.return_value = {
            "updated_at": "2099-01-01T00:00:00Z"  # Far future = drifted
        }

        config = {"domain": "test.edu", "course_id": TEST_COURSE_ID,
                  "headers": {}, "base_url": "", "course_url": ""}
        drifts = progress_tracker.check_drift(TEST_COURSE_ID, config)

        # Should have called with the stored slug first
        first_call_slug = mock_get_page.call_args_list[0][0][1]
        self.assertEqual(first_call_slug, "m1-lesson-cellular-biology")
        self.assertEqual(len(drifts), 1)
        self.assertEqual(drifts[0]["page"], "lesson")

    @patch("canvas_api.get_page")
    def test_falls_back_to_canonical_slug(self, mock_get_page):
        """Without stored slug, tries m{N}-{type} then module-{N}-{type}."""
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete")
        # No slug stored

        # First call (m1-overview) returns None, second (module-1-overview) too
        mock_get_page.return_value = None

        config = {"domain": "test.edu", "course_id": TEST_COURSE_ID,
                  "headers": {}, "base_url": "", "course_url": ""}
        drifts = progress_tracker.check_drift(TEST_COURSE_ID, config)

        # Should have tried both canonical patterns
        slugs_tried = [c[0][1] for c in mock_get_page.call_args_list]
        self.assertIn("m1-overview", slugs_tried)
        self.assertIn("module-1-overview", slugs_tried)
        # No drift since page not found
        self.assertEqual(len(drifts), 0)

    @patch("canvas_api.get_page")
    def test_no_drift_when_timestamps_match(self, mock_get_page):
        """No drift reported when Canvas updated_at <= local updatedAt."""
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "complete",
            slug="m1-overview")

        # Set Canvas timestamp to be older than local
        mock_get_page.return_value = {
            "updated_at": "2020-01-01T00:00:00Z"
        }

        config = {"domain": "test.edu", "course_id": TEST_COURSE_ID,
                  "headers": {}, "base_url": "", "course_url": ""}
        drifts = progress_tracker.check_drift(TEST_COURSE_ID, config)
        self.assertEqual(len(drifts), 0)


class TestSetHelpers(unittest.TestCase):
    """set_module_title and set_phase work correctly."""

    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_set_module_title(self):
        progress_tracker.update_progress(
            TEST_COURSE_ID, 1, "overview", "pending")
        progress_tracker.set_module_title(TEST_COURSE_ID, 1, "Intro to Bio")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(progress["modules"]["1"]["title"], "Intro to Bio")

    def test_set_module_title_creates_module(self):
        """set_module_title should work even if module doesn't exist yet."""
        progress_tracker.set_module_title(TEST_COURSE_ID, 3, "Advanced Topics")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(progress["modules"]["3"]["title"], "Advanced Topics")

    def test_set_phase(self):
        progress_tracker.set_phase(TEST_COURSE_ID, "content")
        progress = progress_tracker.load_progress(TEST_COURSE_ID)
        self.assertEqual(progress["phase"], "content")


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