from __future__ import annotations

import importlib.util
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest import mock


REPO_ROOT = Path(__file__).resolve().parents[1]
DOCTOR_PATH = REPO_ROOT / "python-cli-template" / "doctor.py"


def load_module():
    spec = importlib.util.spec_from_file_location("doctor", DOCTOR_PATH)
    module = importlib.util.module_from_spec(spec)
    assert spec.loader is not None
    spec.loader.exec_module(module)
    return module


class DoctorTests(unittest.TestCase):
    def test_load_dotenv_sets_missing_values_only(self):
        module = load_module()

        with tempfile.TemporaryDirectory() as tmp_dir:
            env_path = Path(tmp_dir) / ".env"
            env_path.write_text(
                "PROJECT_BIN=/tmp/from-dotenv\nPROJECT_CMD_TIMEOUT_SEC=90\n# comment\n",
                encoding="utf-8",
            )
            with mock.patch.dict(os.environ, {"PROJECT_BIN": "/tmp/already-set"}, clear=False):
                module._load_dotenv(env_path)
                self.assertEqual(os.environ["PROJECT_BIN"], "/tmp/already-set")
                self.assertEqual(os.environ["PROJECT_CMD_TIMEOUT_SEC"], "90")

    def test_validate_server_entry_accepts_valid_shape(self):
        module = load_module()
        error = module._validate_server_entry(
            "demo-server",
            {
                "type": "stdio",
                "command": "/abs/path/python",
                "args": ["/abs/path/server.py"],
                "env": {"DEMO_SERVER_BIN": "/abs/path/demo-cli"},
            },
        )
        self.assertIsNone(error)

    def test_validate_server_entry_rejects_invalid_shapes(self):
        module = load_module()

        self.assertIn("non-empty 'command' string", module._validate_server_entry("demo", {}))
        self.assertIn(
            "string-only 'args' array",
            module._validate_server_entry("demo", {"command": "python", "args": [1]}),
        )
        self.assertIn(
            "should include an 'args' entry",
            module._validate_server_entry("demo", {"command": "python"}),
        )
        self.assertIn(
            "string keys and values in 'env'",
            module._validate_server_entry(
                "demo",
                {"command": "python", "args": ["server.py"], "env": {"OK": 1}},
            ),
        )

    def test_check_config_accepts_sample_config_shape(self):
        module = load_module()

        with tempfile.TemporaryDirectory() as tmp_dir:
            config_path = Path(tmp_dir) / "config.json"
            config_path.write_text(
                json.dumps(
                    {
                        "mcpServers": {
                            "demo-server": {
                                "type": "stdio",
                                "command": "/abs/path/python",
                                "args": ["/abs/path/server.py"],
                                "env": {"DEMO_SERVER_BIN": "/abs/path/demo-cli"},
                            }
                        }
                    }
                ),
                encoding="utf-8",
            )

            self.assertTrue(module._check_config(config_path)["ok"])

    def test_check_config_rejects_bad_server_entry(self):
        module = load_module()

        with tempfile.TemporaryDirectory() as tmp_dir:
            config_path = Path(tmp_dir) / "config.json"
            config_path.write_text(
                json.dumps(
                    {
                        "mcpServers": {
                            "demo-server": {
                                "command": "",
                                "args": [],
                            }
                        }
                    }
                ),
                encoding="utf-8",
            )

            self.assertFalse(module._check_config(config_path)["ok"])

    def test_run_checks_json_shape(self):
        module = load_module()

        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_path = Path(tmp_dir)
            fake_bin = tmp_path / "project-cli"
            fake_bin.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
            fake_bin.chmod(0o755)
            read_dir = tmp_path / "read"
            write_dir = tmp_path / "write"
            read_dir.mkdir()
            write_dir.mkdir()
            config_path = tmp_path / "config.json"
            config_path.write_text(
                json.dumps(
                    {
                        "mcpServers": {
                            "demo-server": {
                                "command": "/abs/path/python",
                                "args": ["/abs/path/server.py"],
                            }
                        }
                    }
                ),
                encoding="utf-8",
            )
            server_path = tmp_path / "server.py"
            server_path.write_text("print('ok')\n", encoding="utf-8")

            with mock.patch.dict(
                os.environ,
                {
                    "PROJECT_BIN": str(fake_bin),
                    "PROJECT_ALLOWED_READ_DIRS": str(read_dir),
                    "PROJECT_ALLOWED_WRITE_DIRS": str(write_dir),
                },
                clear=False,
            ):
                results = module._run_checks(server_path, [config_path])

        self.assertTrue(all(isinstance(result, dict) for result in results))
        self.assertTrue(all({"ok", "label", "detail"} <= set(result) for result in results))


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