"""Tests for hooks/inject.py. Run: python3 tests/hook_test.py"""

import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory

PLUGIN_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PLUGIN_ROOT / "hooks"))

import inject  # noqa: E402


def write_prompts(directory, files):
    """files: dict of filename -> content, written under directory."""
    directory.mkdir(parents=True, exist_ok=True)
    for name, content in files.items():
        (directory / name).write_text(content)


class ComposeOrderTest(unittest.TestCase):
    def test_concatenates_in_filename_sort_order(self):
        with TemporaryDirectory() as tmp:
            d = Path(tmp)
            write_prompts(d, {"20-second.md": "second\n", "10-first.md": "first\n"})
            paths = inject.prompt_files(d)
            self.assertEqual(["10-first.md", "20-second.md"], [Path(p).name for p in paths])
            self.assertEqual("first\nsecond\n", inject.compose(paths))

    def test_no_separator_added_between_files(self):
        with TemporaryDirectory() as tmp:
            d = Path(tmp)
            write_prompts(d, {"10-a.md": "a", "20-b.md": "b"})
            paths = inject.prompt_files(d)
            self.assertEqual("ab", inject.compose(paths))


class ByteIdentityTest(unittest.TestCase):
    """With only the real 10-orchestration.md present, the composer's output
    must be byte-identical to what the old single-file inject.py emitted
    directly from ORCHESTRATION.md's contents — no added separators or
    trailing newlines."""

    def test_single_file_matches_its_raw_contents_exactly(self):
        real_file = PLUGIN_ROOT / "prompts" / "session-start" / "10-orchestration.md"
        with TemporaryDirectory() as tmp:
            d = Path(tmp)
            d.mkdir(exist_ok=True)
            shadow = d / "10-orchestration.md"
            shadow.write_bytes(real_file.read_bytes())
            paths = inject.prompt_files(d)
            composed = inject.compose(paths)
            self.assertEqual(real_file.read_text(), composed)

    def test_real_prompts_dir_composes_all_files_byte_identical_concatenation(self):
        paths = inject.prompt_files()
        self.assertGreaterEqual(len(paths), 1)
        expected = "".join(Path(p).read_text() for p in paths)
        self.assertEqual(expected, inject.compose(paths))


class BudgetTest(unittest.TestCase):
    def make_dir_with_lines(self, tmp, n_lines):
        d = Path(tmp)
        d.mkdir(exist_ok=True)
        content = "\n".join(f"line {i}" for i in range(n_lines)) + "\n"
        (d / "10-big.md").write_text(content)
        return d

    def test_under_warn_budget_injects_silently(self):
        with TemporaryDirectory() as tmp:
            d = self.make_dir_with_lines(tmp, inject.WARN_LINE_BUDGET - 1)
            paths = inject.prompt_files(d)
            context = inject.compose(paths)
            self.assertLessEqual(len(context.splitlines()), inject.WARN_LINE_BUDGET)

    def test_over_warn_under_refuse_still_injects(self):
        with TemporaryDirectory() as tmp:
            d = self.make_dir_with_lines(
                tmp, inject.WARN_LINE_BUDGET + 1
            )
            paths = inject.prompt_files(d)
            context = inject.compose(paths)
            line_count = len(context.splitlines())
            self.assertGreater(line_count, inject.WARN_LINE_BUDGET)
            self.assertLessEqual(line_count, inject.REFUSE_LINE_BUDGET)
            # Injection still happens: compose() returns the full content.
            self.assertTrue(context)

    def test_over_refuse_budget_line_count_detected(self):
        with TemporaryDirectory() as tmp:
            d = self.make_dir_with_lines(tmp, inject.REFUSE_LINE_BUDGET + 1)
            paths = inject.prompt_files(d)
            context = inject.compose(paths)
            self.assertGreater(len(context.splitlines()), inject.REFUSE_LINE_BUDGET)

    def test_main_warns_on_stderr_between_warn_and_refuse(self, ):
        import io
        from unittest import mock

        with TemporaryDirectory() as tmp:
            d = self.make_dir_with_lines(tmp, inject.WARN_LINE_BUDGET + 1)
            stderr = io.StringIO()
            stdout = io.StringIO()
            with mock.patch.object(inject, "SESSION_START_DIR", str(d)), \
                 mock.patch("sys.stderr", stderr), \
                 mock.patch("sys.stdout", stdout):
                inject.main()
            self.assertIn("injecting anyway", stderr.getvalue())
            payload = json.loads(stdout.getvalue())
            self.assertIn("additionalContext", payload["hookSpecificOutput"])

    def test_main_refuses_and_emits_nothing_over_refuse_budget(self):
        import io
        from unittest import mock

        with TemporaryDirectory() as tmp:
            d = self.make_dir_with_lines(tmp, inject.REFUSE_LINE_BUDGET + 1)
            stderr = io.StringIO()
            stdout = io.StringIO()
            with mock.patch.object(inject, "SESSION_START_DIR", str(d)), \
                 mock.patch("sys.stderr", stderr), \
                 mock.patch("sys.stdout", stdout):
                inject.main()
            self.assertIn("refusing to inject", stderr.getvalue())
            self.assertEqual("", stdout.getvalue())


class NoPromptsTest(unittest.TestCase):
    def test_main_emits_nothing_when_dir_empty(self):
        import io
        from unittest import mock

        with TemporaryDirectory() as tmp:
            d = Path(tmp) / "empty"
            d.mkdir()
            stdout = io.StringIO()
            with mock.patch.object(inject, "SESSION_START_DIR", str(d)), \
                 mock.patch("sys.stdout", stdout):
                inject.main()
            self.assertEqual("", stdout.getvalue())


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