#!/usr/bin/env python3
"""Tests for the transcript pipeline: SRT support, auto-discovery, placeholder
replacement, sentence-aware paragraphing, and the after-embed fallback.
"""

import os
import sys
import tempfile
import unittest
from unittest import mock

HERE = os.path.dirname(os.path.abspath(__file__))
SCRIPTS = os.path.join(HERE, "..", "scripts")
sys.path.insert(0, SCRIPTS)

import add_transcripts as at  # noqa: E402
import canvas_api  # noqa: E402


class SrtToTextTest(unittest.TestCase):
    def test_strips_indices_and_timestamps(self):
        srt_content = (
            "1\n"
            "00:00:00,000 --> 00:00:02,000\n"
            "Hello world.\n"
            "\n"
            "2\n"
            "00:00:02,000 --> 00:00:04,000\n"
            "This is a test.\n"
        )
        with tempfile.NamedTemporaryFile(
                mode="w", suffix=".srt", delete=False) as f:
            f.write(srt_content)
            path = f.name
        try:
            text = at.srt_to_text(path)
        finally:
            os.remove(path)
        self.assertEqual(text, "Hello world. This is a test.")

    def test_transcript_to_text_dispatches_on_extension(self):
        with tempfile.NamedTemporaryFile(
                mode="w", suffix=".srt", delete=False) as f:
            f.write("1\n00:00:00,000 --> 00:00:01,000\nHi.\n")
            srt_path = f.name
        with tempfile.NamedTemporaryFile(
                mode="w", suffix=".vtt", delete=False) as f:
            f.write("WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nHi.\n")
            vtt_path = f.name
        try:
            self.assertEqual(at.transcript_to_text(srt_path), "Hi.")
            self.assertEqual(at.transcript_to_text(vtt_path), "Hi.")
        finally:
            os.remove(srt_path)
            os.remove(vtt_path)


class ParagraphizeTest(unittest.TestCase):
    def test_multi_sentence_transcript_yields_multiple_paragraphs(self):
        text = " ".join(f"Sentence number {i}." for i in range(1, 13))
        paragraphs = at.paragraphize(text, sentences_per_para=5)
        self.assertGreater(len(paragraphs), 1)
        self.assertEqual(len(paragraphs), 3)  # 5 + 5 + 2 sentences

    def test_make_transcript_block_has_multiple_paragraphs(self):
        text = " ".join(f"Sentence number {i}." for i in range(1, 13))
        block = at.make_transcript_block(text, "video")
        self.assertGreater(block.count("<p>"), 1)
        # Existing scrollable <details> container is preserved.
        self.assertIn("<details", block)
        self.assertIn("max-height: 400px; overflow-y: auto", block)


class PlaceholderReplacementTest(unittest.TestCase):
    def test_replaces_each_placeholder_and_leaves_none_behind(self):
        body = (
            "<p>before</p>"
            '<p style="font-size:15px;color:#595959;line-height:1.6;margin:8px 0 0;">'
            '<span style="background-color:#fbeeb8;">[Transcript here]</span></p>'
            "<p>middle</p>"
            "<p><span>[Transcript here]</span></p>"
            "<p>after</p>"
        )
        blocks = [
            at.make_transcript_block("Hello there.", "video"),
            at.make_transcript_block("Second one.", "video"),
        ]
        new_body, n = at.replace_placeholders(body, blocks)
        self.assertEqual(n, 2)
        self.assertEqual(new_body.count("<details"), 2)
        self.assertNotIn("[Transcript here]", new_body)

    def test_add_transcript_to_page_fills_placeholder(self):
        body = (
            "<p>before</p>"
            '<p style="font-size:15px;"><span style="background-color:#fbeeb8;">'
            "[Transcript here]</span></p>"
        )
        config = {"course_id": "123"}
        staged = {}

        def _fake_stage(course_id, object_type, object_id, content, **kwargs):
            staged["content"] = content
            return {"ok": True}

        with mock.patch.object(
                at, "get_page", return_value={"body": body, "title": "T"}), \
             mock.patch.object(
                 at, "transcript_to_text", return_value="Hello there."), \
             mock.patch("os.path.exists", return_value=True), \
             mock.patch.object(at.staging_manager, "stage", side_effect=_fake_stage):
            result = at.add_transcript_to_page(
                config, "m1-lesson", "/tmp/fake.vtt", "video", 999, "m1")

        self.assertTrue(result)
        self.assertNotIn("[Transcript here]", staged["content"])
        # Placeholder fill inserts only the scrollable body, never a <details>.
        self.assertEqual(staged["content"].count("<details"), 0)
        self.assertIn("overflow-y: auto", staged["content"])

    def test_placeholder_inside_details_does_not_nest(self):
        # The real target template wraps the placeholder in a faculty
        # <details> disclosure. Filling it must NOT add a second <details>,
        # or the student has to click twice to read the transcript.
        body = (
            "<details><summary>Transcript</summary>"
            '<p style="font-size:15px;"><span style="background-color:#fbeeb8;">'
            "[Transcript here]</span></p>"
            "</details>"
        )
        config = {"course_id": "123"}
        staged = {}

        def _fake_stage(course_id, object_type, object_id, content, **kwargs):
            staged["content"] = content
            return {"ok": True}

        multi = " ".join(f"Sentence {i}." for i in range(1, 13))
        with mock.patch.object(
                at, "get_page", return_value={"body": body, "title": "T"}), \
             mock.patch.object(
                 at, "transcript_to_text", return_value=multi), \
             mock.patch("os.path.exists", return_value=True), \
             mock.patch.object(at.staging_manager, "stage", side_effect=_fake_stage):
            result = at.add_transcript_to_page(
                config, "m1-lesson", "/tmp/fake.vtt", "video", 999, "m1")

        self.assertTrue(result)
        # Still exactly one <details> (the faculty one) -- no nesting.
        self.assertEqual(staged["content"].count("<details"), 1)
        self.assertNotIn("[Transcript here]", staged["content"])
        self.assertIn("overflow-y: auto", staged["content"])
        self.assertGreater(staged["content"].count("<p>"), 1)


class AfterEmbedFallbackTest(unittest.TestCase):
    def test_inserts_after_embed_when_no_placeholder(self):
        body = '<p>Intro</p><a href="/files/456">Video</a></a><h2>Next</h2>'
        block = at.make_transcript_block("Hello world.", "video")
        new_body = at._insert_after_embed(body, block, 456, "m1", "video")
        self.assertIn(block, new_body)
        # Inserted right after the closing </a>, before <h2>Next</h2>.
        self.assertLess(new_body.index(block), new_body.index("<h2>Next</h2>"))

    def test_add_transcript_to_page_uses_fallback_when_no_placeholder(self):
        body = '<p>Intro</p><a href="/files/456">Video</a></a><h2>Next</h2>'
        config = {"course_id": "123"}
        staged = {}

        def _fake_stage(course_id, object_type, object_id, content, **kwargs):
            staged["content"] = content
            return {"ok": True}

        with mock.patch.object(
                at, "get_page", return_value={"body": body, "title": "T"}), \
             mock.patch.object(
                 at, "transcript_to_text", return_value="Hello world."), \
             mock.patch("os.path.exists", return_value=True), \
             mock.patch.object(at.staging_manager, "stage", side_effect=_fake_stage):
            result = at.add_transcript_to_page(
                config, "m1-lesson", "/tmp/fake.vtt", "video", 456, "m1")

        self.assertTrue(result)
        self.assertIn("<details", staged["content"])


class DiscoverPageMappingTest(unittest.TestCase):
    def test_maps_iframe_ids_in_order_respecting_placeholder_count(self):
        body = (
            "<p>intro</p>"
            '<iframe src="https://fast.wistia.net/embed/iframe/aaa111"></iframe>'
            '<p style="font-size:15px;">'
            '<span style="background-color:#fbeeb8;">[Transcript here]</span></p>'
            '<iframe src="https://fast.wistia.net/embed/iframe/bbb222"></iframe>'
            "<p><span>[Transcript here]</span></p>"
            '<iframe src="https://fast.wistia.net/embed/iframe/ccc333"></iframe>'
        )
        config = {"course_id": "123"}

        with mock.patch.object(at, "get_modules", return_value=[{"id": 1}]), \
             mock.patch.object(
                 at, "get_module_items",
                 return_value=[{"type": "Page", "page_url": "m1-lesson"}]), \
             mock.patch.object(
                 at, "get_page", return_value={"body": body, "title": "Lesson"}):
            mapping, missing = at.discover_page_mapping(config, "/tmp/no-such-dir-xyz")

        # Only the first 2 ids are taken: there are 2 placeholders, 3 ids.
        self.assertEqual(mapping, {"m1-lesson": ["aaa111", "bbb222"]})
        self.assertEqual(missing, ["aaa111", "bbb222"])

    def test_skips_pages_with_no_placeholders(self):
        body = (
            '<iframe src="https://fast.wistia.net/embed/iframe/aaa111"></iframe>'
        )
        config = {"course_id": "123"}

        with mock.patch.object(at, "get_modules", return_value=[{"id": 1}]), \
             mock.patch.object(
                 at, "get_module_items",
                 return_value=[{"type": "Page", "page_url": "m1-lesson"}]), \
             mock.patch.object(
                 at, "get_page", return_value={"body": body, "title": "Lesson"}):
            mapping, missing = at.discover_page_mapping(config, "/tmp/no-such-dir-xyz")

        self.assertEqual(mapping, {})
        self.assertEqual(missing, [])


class RunAutoTest(unittest.TestCase):
    def test_dry_run_writes_nothing(self):
        config = {"course_id": "123"}
        with mock.patch.object(
                at, "discover_page_mapping",
                return_value=({"m1-lesson": ["aaa111"]}, ["aaa111"])), \
             mock.patch.object(at.staging_manager, "stage") as stg:
            code = at.run_auto(config, "/tmp/no-such-dir-xyz", dry_run=True)
        self.assertEqual(code, 0)
        stg.assert_not_called()

    def test_stages_when_transcript_files_present(self):
        config = {"course_id": "123"}
        page_body = "<p>[Transcript here]</p>"
        staged = {}

        def _fake_stage(course_id, object_type, object_id, content, **kwargs):
            staged["content"] = content
            return {"ok": True}

        with mock.patch.object(
                at, "discover_page_mapping",
                return_value=({"m1-lesson": ["aaa111"]}, [])), \
             mock.patch.object(
                 at, "_find_transcript_file", return_value="/tmp/aaa111.srt"), \
             mock.patch.object(
                 at, "get_page", return_value={"body": page_body, "title": "T"}), \
             mock.patch.object(
                 at, "transcript_to_text", return_value="Hello there."), \
             mock.patch.object(at.staging_manager, "stage", side_effect=_fake_stage), \
             mock.patch.object(canvas_api, "update_page") as upd:
            code = at.run_auto(config, "/tmp/no-such-dir-xyz", dry_run=False)

        self.assertEqual(code, 0)
        # Auto fill inserts only the scrollable body (placeholders live inside
        # faculty <details>), and never writes directly to Canvas.
        self.assertIn("overflow-y: auto", staged["content"])
        self.assertNotIn("[Transcript here]", staged["content"])
        upd.assert_not_called()


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