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

from mixersystem.sync import PACKAGE_DIR, sync


def _write_doc(directory: Path, name: str, content: str = "") -> None:
    """Create a _name.md module doc file with plain markdown (no frontmatter)."""
    directory.mkdir(parents=True, exist_ok=True)
    body = content or f"# {name}\n"
    (directory / f"_{name}.md").write_text(body)


class SyncSettingsTest(unittest.TestCase):
    def test_sync_creates_linear_block_defaults(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "app")

            sync(project_root=tmpdir)

            settings = json.loads((root / ".mixer" / "settings.json").read_text())
            self.assertIn("modules", settings)
            self.assertIn("app", settings["modules"])
            self.assertEqual(settings["modules"]["app"]["doc"], "./app/_app.md")
            self.assertIn("children", settings["modules"]["app"])
            self.assertEqual(settings["linear"]["team_prefix"], "")
            self.assertEqual(settings["linear"]["team_id"], "")

    def test_sync_preserves_existing_linear_values(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "app")
            (root / ".mixer").mkdir(parents=True, exist_ok=True)

            existing = {
                "modules": {},
                "linear": {
                    "team_prefix": "ENG",
                    "team_id": "team_123",
                    "extra": "keep-me",
                },
                "custom": {"x": 1},
            }
            (root / ".mixer" / "settings.json").write_text(json.dumps(existing, indent=2) + "\n")

            sync(project_root=tmpdir)

            settings = json.loads((root / ".mixer" / "settings.json").read_text())
            self.assertEqual(settings["linear"]["team_prefix"], "ENG")
            self.assertEqual(settings["linear"]["team_id"], "team_123")
            self.assertEqual(settings["linear"]["extra"], "keep-me")
            self.assertEqual(settings["custom"]["x"], 1)
            self.assertIn("app", settings["modules"])

    def test_sync_builds_hierarchical_tree(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "app")
            _write_doc(root / "app" / "cli", "cli")
            _write_doc(root / "app" / "generator", "generator")

            sync(project_root=tmpdir)

            settings = json.loads((root / ".mixer" / "settings.json").read_text())
            app = settings["modules"]["app"]
            self.assertEqual(app["doc"], "./app/_app.md")
            self.assertIn("cli", app["children"])
            self.assertIn("generator", app["children"])
            self.assertEqual(app["children"]["cli"]["doc"], "./app/cli/_cli.md")
            self.assertEqual(app["children"]["generator"]["doc"], "./app/generator/_generator.md")

    def test_sync_nodes_have_only_doc_and_children(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "app")

            sync(project_root=tmpdir)

            settings = json.loads((root / ".mixer" / "settings.json").read_text())
            app = settings["modules"]["app"]
            self.assertEqual(set(app.keys()), {"doc", "children"})

    def test_sync_raises_on_duplicate_names(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "mymod")
            _write_doc(root / "lib", "mymod")

            with self.assertRaises(ValueError) as ctx:
                sync(project_root=tmpdir)
            self.assertIn("Duplicate module name", str(ctx.exception))

    def test_sync_ignores_non_underscore_md_files(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "app")
            # Regular .md file without underscore prefix — should be ignored
            (root / "lib").mkdir(parents=True, exist_ok=True)
            (root / "lib" / "README.md").write_text("# Not a module doc\n")

            sync(project_root=tmpdir)

            settings = json.loads((root / ".mixer" / "settings.json").read_text())
            self.assertIn("app", settings["modules"])
            self.assertNotIn("lib", settings["modules"])

    def test_sync_raises_on_multiple_doc_files_in_directory(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            (root / "app").mkdir(parents=True, exist_ok=True)
            (root / "app" / "_app.md").write_text("# App\n")
            (root / "app" / "_extra.md").write_text("# Extra\n")

            with self.assertRaises(ValueError) as ctx:
                sync(project_root=tmpdir)
            self.assertIn("Multiple _*.md", str(ctx.exception))

    def test_sync_raises_when_no_modules_found(self) -> None:
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            (root / "app").mkdir(parents=True, exist_ok=True)
            (root / "app" / "README.md").write_text("# Not a module doc\n")

            with self.assertRaises(FileNotFoundError):
                sync(project_root=tmpdir)

    def test_sync_preserves_user_added_module_keys(self) -> None:
        """User-added keys on module entries must survive re-sync."""
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "app")
            _write_doc(root / "app" / "cli", "cli")
            (root / ".mixer").mkdir(parents=True, exist_ok=True)

            existing = {
                "modules": {
                    "app": {
                        "doc": "./app/_app.md",
                        "children": {
                            "cli": {
                                "doc": "./app/cli/_cli.md",
                                "children": {},
                                "user_note": "keep me",
                            }
                        },
                        "custom_flag": True,
                    }
                },
                "linear": {"team_prefix": "ENG", "team_id": "t1"},
            }
            (root / ".mixer" / "settings.json").write_text(
                json.dumps(existing, indent=2) + "\n"
            )

            sync(project_root=tmpdir)

            settings = json.loads((root / ".mixer" / "settings.json").read_text())
            app = settings["modules"]["app"]
            self.assertTrue(app["custom_flag"])
            self.assertEqual(app["children"]["cli"]["user_note"], "keep me")

    def test_sync_does_not_remove_modules_missing_from_scan(self) -> None:
        """Modules in settings.json that are no longer on disk should be kept."""
        with TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            _write_doc(root / "app", "app")
            (root / ".mixer").mkdir(parents=True, exist_ok=True)

            existing = {
                "modules": {
                    "app": {"doc": "./app/_app.md", "children": {}},
                    "old_module": {"doc": "./old/_old_module.md", "children": {}},
                },
                "linear": {"team_prefix": "", "team_id": ""},
            }
            (root / ".mixer" / "settings.json").write_text(
                json.dumps(existing, indent=2) + "\n"
            )

            sync(project_root=tmpdir)

            settings = json.loads((root / ".mixer" / "settings.json").read_text())
            self.assertIn("app", settings["modules"])
            self.assertIn("old_module", settings["modules"])


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