import os
import stat
import tempfile
import unittest
from pathlib import Path

import yaml

from app.runtime_config import validate_runtime_config
from app.runtime_defaults import (
    ACTIVE_LEARNING_DEFAULT,
    STREAM_AUTOSTART_DEFAULT,
    STREAM_START_DEFAULT,
)
from app.runtime_health import collect_runtime_health
from app.runtime_state import write_yaml_atomic
from tools.validate_deployment import validate_deployment_inputs


class _Probe:
    def __init__(self, result=True, error=None):
        self.result = result
        self.error = error

    def ping(self):
        if self.error:
            raise self.error
        return self.result


class _Engine:
    def __init__(self, result=True, error=None):
        self.result = result
        self.error = error

    def is_model_ready(self, model_name, force_refresh=False):
        self.last_call = (model_name, force_refresh)
        if self.error:
            raise self.error
        return self.result


class RuntimeHealthTest(unittest.TestCase):
    def test_ready_only_when_every_dependency_passes(self):
        engine = _Engine()
        result = collect_runtime_health(engine, _Probe(), _Probe(), "RFDETRMedium")
        self.assertTrue(result["ready"])
        self.assertEqual(("RFDETRMedium", True), engine.last_call)

    def test_failure_is_bounded_and_not_ready(self):
        result = collect_runtime_health(
            _Engine(result=False),
            _Probe(error=RuntimeError("secret must not leak")),
            _Probe(),
            "RFDETRMedium",
        )
        self.assertFalse(result["ready"])
        self.assertEqual("not_ready", result["status"])
        self.assertNotIn("secret must not leak", str(result))


class RuntimeConfigTest(unittest.TestCase):
    def _environment(self, root):
        root = Path(root)
        state = root / "state"
        state.mkdir(mode=0o700)
        files = {
            "SYSTEM_CONFIG_PATH": root / "system.yaml",
            "STREAMS_CONFIG_PATH": root / "streams.yaml",
            "CAMERA_PTZ_CONFIG_PATH": root / "camera.yaml",
        }
        files["SYSTEM_CONFIG_PATH"].write_text("active_learning:\n  enabled: false\n")
        files["STREAMS_CONFIG_PATH"].write_text("streams: []\n")
        files["CAMERA_PTZ_CONFIG_PATH"].write_text("cameras: {}\n")
        return {**{key: str(value) for key, value in files.items()},
                "STREAMS_RUNTIME_PATH": str(state / "streams_runtime.yaml")}

    def test_valid_empty_runtime_config(self):
        with tempfile.TemporaryDirectory() as root:
            summary = validate_runtime_config(self._environment(root))
        self.assertEqual(0, summary["stream_count"])
        self.assertFalse(summary["active_learning"])

    def test_missing_config_fails(self):
        with tempfile.TemporaryDirectory() as root:
            environment = self._environment(root)
            os.unlink(environment["STREAMS_CONFIG_PATH"])
            with self.assertRaisesRegex(ValueError, "streams config file is missing"):
                validate_runtime_config(environment)

    def test_invalid_yaml_fails(self):
        with tempfile.TemporaryDirectory() as root:
            environment = self._environment(root)
            Path(environment["SYSTEM_CONFIG_PATH"]).write_text("active_learning: [\n")
            with self.assertRaisesRegex(ValueError, "system config is not valid YAML"):
                validate_runtime_config(environment)


class RuntimeStateTest(unittest.TestCase):
    def test_atomic_state_uses_private_modes(self):
        with tempfile.TemporaryDirectory() as root:
            path = Path(root) / "runtime" / "streams_runtime.yaml"
            write_yaml_atomic(str(path), {"added": [], "removed": []})
            payload = yaml.safe_load(path.read_text())
            self.assertEqual({"added": [], "removed": []}, payload)
            self.assertEqual(0o600, stat.S_IMODE(path.stat().st_mode))
            self.assertEqual(0o700, stat.S_IMODE(path.parent.stat().st_mode))


class SafeDefaultsTest(unittest.TestCase):
    def test_stream_and_artifact_defaults_are_off(self):
        self.assertFalse(STREAM_START_DEFAULT)
        self.assertFalse(STREAM_AUTOSTART_DEFAULT)
        self.assertFalse(ACTIVE_LEARNING_DEFAULT)


class DeploymentPreflightTest(unittest.TestCase):
    def _layout(self, root):
        root = Path(root)
        config_root = root / "config"
        config_dir = config_root / "configs"
        state_root = root / "state"
        model_root = root / "models"
        model = model_root / "triton" / "RFDETRMedium"
        for directory in (config_root, config_dir, state_root):
            directory.mkdir(mode=0o700, parents=True, exist_ok=True)
            os.chmod(directory, 0o700)
        model.mkdir(parents=True)
        (model / "config.pbtxt").write_text('name: "RFDETRMedium"\n')
        for path, content in (
            (config_dir / "system_config.yaml", "active_learning:\n  enabled: false\n"),
            (config_dir / "streams.yaml", "streams: []\n"),
            (config_dir / "camera_ptz_config.yaml", "cameras: {}\n"),
            (state_root / "streams_runtime.yaml", "added: []\nremoved: []\n"),
        ):
            path.write_text(content)
            os.chmod(path, 0o600)
        env = root / "runtime.env"
        env.write_text(
            "\n".join((
                "CONTEXTRA_IMAGE=contextra-analytics:test",
                "FASTAPI_HOST_PORT=35000",
                "GPU_DEVICE_ID=0",
                "ANALYTICS_RESTART_POLICY=on-failure:3",
                "INFRA_RESTART_POLICY=unless-stopped",
                f"CONTEXTRA_CONFIG_ROOT={config_root}",
                f"CONTEXTRA_RUNTIME_STATE_ROOT={state_root}",
                f"CONTEXTRA_MODEL_ROOT={model_root}",
                "POSTGRES_DB=contextra",
                "POSTGRES_USER=contextra",
                "POSTGRES_PASSWORD=0123456789abcdef",
                "GRAFANA_ADMIN_PASSWORD=fedcba9876543210",
                "MODEL_NAME=RFDETRMedium",
                "",
            ))
        )
        os.chmod(env, 0o600)
        return env, config_root

    def test_preflight_accepts_complete_private_layout(self):
        with tempfile.TemporaryDirectory() as root:
            env, _ = self._layout(root)
            result = validate_deployment_inputs(str(env))
        self.assertEqual("RFDETRMedium", result["model_name"])

    def test_preflight_rejects_missing_bind_source(self):
        with tempfile.TemporaryDirectory() as root:
            env, config_root = self._layout(root)
            for line in env.read_text().splitlines():
                if line.startswith("CONTEXTRA_CONFIG_ROOT="):
                    old = line
                    break
            env.write_text(env.read_text().replace(old, f"CONTEXTRA_CONFIG_ROOT={config_root}-missing"))
            os.chmod(env, 0o600)
            with self.assertRaisesRegex(ValueError, "Config root directory is missing"):
                validate_deployment_inputs(str(env))


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