"""Tests for census doctor — self-diagnostic checks."""

from __future__ import annotations

import platform
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from certisigma_census.doctor import (
    DOCTOR_HTTP_TIMEOUT_SEC,
    STATUS_FAIL,
    STATUS_OK,
    STATUS_WARN,
    check_api_connectivity,
    check_api_key,
    check_census_version,
    check_config,
    check_inotify,
    check_manifest,
    check_optional_deps,
    check_python_version,
    check_sdk_version,
    run_doctor,
)


class TestCheckPython:
    def test_python_ok(self):
        result = check_python_version()
        assert result.status == STATUS_OK
        assert "Python" in result.message

    def test_census_version(self):
        result = check_census_version()
        assert result.status == STATUS_OK
        assert "certisigma-census" in result.message


class TestCheckSDK:
    def test_sdk_installed(self):
        result = check_sdk_version()
        assert result.status == STATUS_OK
        assert "certisigma" in result.message


class TestCheckOptionalDeps:
    def test_returns_list(self):
        results = check_optional_deps()
        assert len(results) == 2
        names = {r.name for r in results}
        assert "watchdog" in names
        assert "fpdf2" in names


class TestCheckConfig:
    def test_returns_user_and_project(self):
        results = check_config()
        assert len(results) == 2
        names = {r.name for r in results}
        assert "config_user" in names
        assert "config_project" in names


class TestCheckAPIConnectivity:
    @patch("certisigma.CertiSigmaClient")
    def test_api_reachable(self, mock_cls):
        mock_cls.return_value.health.return_value = {"status": "ok"}
        result = check_api_connectivity()
        assert result.status == STATUS_OK
        assert "reachable" in result.message

    @patch("certisigma.CertiSigmaClient")
    def test_api_unreachable(self, mock_cls):
        mock_cls.return_value.health.side_effect = ConnectionError("timeout")
        result = check_api_connectivity()
        assert result.status == STATUS_FAIL
        assert "unreachable" in result.message

    @patch("certisigma.CertiSigmaClient")
    def test_uses_short_http_timeout(self, mock_cls):
        mock_cls.return_value.health.return_value = {"status": "ok"}
        check_api_connectivity()
        assert mock_cls.call_args.kwargs.get("timeout") == DOCTOR_HTTP_TIMEOUT_SEC

    @patch("certisigma.CertiSigmaClient")
    def test_passes_base_url_with_timeout(self, mock_cls):
        mock_cls.return_value.health.return_value = {"status": "ok"}
        check_api_connectivity("https://custom.example/")
        kw = mock_cls.call_args.kwargs
        assert kw["base_url"] == "https://custom.example/"
        assert kw["timeout"] == DOCTOR_HTTP_TIMEOUT_SEC


class TestCheckAPIKey:
    @patch("certisigma_census.config.load_config", return_value={"api_key": ""})
    def test_no_key_configured(self, _mock):
        result = check_api_key()
        assert result.status == STATUS_WARN
        assert "No API key" in result.message

    @patch("certisigma_census.config.load_config", return_value={"api_key": "cs_test1234567890"})
    def test_valid_key_format(self, _mock_cfg):
        result = check_api_key()
        assert result.status == STATUS_OK
        assert "configured" in result.message

    @patch("certisigma_census.config.load_config", return_value={"api_key": "bad_prefix_key_1234"})
    def test_bad_key_format(self, _mock_cfg):
        result = check_api_key()
        assert result.status == STATUS_WARN
        assert "unexpected format" in result.message

    def test_explicit_key_overrides_config(self):
        result = check_api_key(api_key="cs_explicit_1234567890")
        assert result.status == STATUS_OK
        assert "...7890" in result.message


class TestCheckInotify:
    def test_returns_none_on_non_linux(self):
        if platform.system() != "Linux":
            assert check_inotify() is None

    @pytest.mark.skipif(platform.system() != "Linux", reason="Linux only")
    def test_returns_result_on_linux(self):
        result = check_inotify()
        assert result is not None
        assert result.name == "inotify"


class TestCheckManifest:
    def test_missing_manifest(self, tmp_path):
        result = check_manifest(tmp_path / "missing.db")
        assert result.status == STATUS_FAIL
        assert "not found" in result.message

    def test_valid_manifest(self, tmp_path):
        from certisigma_census.manifest import Manifest, ManifestEntry

        m = Manifest(root_dir="/data")
        m.add(ManifestEntry(hash_hex="aa" * 32, path="a.txt", size=100, mtime=1.0))
        mp = tmp_path / "test.db"
        m.save(mp)
        m.close()

        result = check_manifest(mp)
        assert result.status == STATUS_OK
        assert "1 files" in result.message

    def test_corrupt_manifest(self, tmp_path):
        bad = tmp_path / "corrupt.db"
        bad.write_bytes(b"not a sqlite database")
        result = check_manifest(bad)
        assert result.status == STATUS_FAIL

    def test_encrypted_manifest_detected(self, tmp_path):
        from certisigma_census.encryption import encrypt_file
        from certisigma_census.manifest import Manifest, ManifestEntry

        m = Manifest(root_dir="/data")
        m.add(ManifestEntry(hash_hex="bb" * 32, path="b.txt", size=50, mtime=1.0))
        db = tmp_path / "test.db"
        m.save(db)
        m.close()

        enc = db.with_suffix(".db.enc")
        encrypt_file(db, enc, "a" * 64)
        db.unlink()

        result = check_manifest(db)
        assert result.status == STATUS_OK
        assert "encrypted" in result.message.lower()

    def test_encrypted_manifest_inline_detected(self, tmp_path):
        """Direct encrypted file (not .enc extension)."""
        from certisigma_census.encryption import HEADER

        enc_file = tmp_path / "manifest.db"
        enc_file.write_bytes(HEADER + b"\x00" * 100)

        result = check_manifest(enc_file)
        assert result.status == STATUS_OK
        assert "encrypted" in result.message.lower()


class TestCheckAPIConnectivityEdge:
    @patch("certisigma.CertiSigmaClient", side_effect=Exception("init failed"))
    def test_client_init_failure(self, _mock):
        result = check_api_connectivity()
        assert result.status == STATUS_FAIL
        assert "unreachable" in result.message


class TestCheckInotifyEdge:
    @pytest.mark.skipif(platform.system() != "Linux", reason="Linux only")
    def test_inotify_returns_warn_or_ok(self):
        result = check_inotify()
        assert result is not None
        assert result.status in (STATUS_OK, STATUS_WARN)
        assert "inotify" in result.name


class TestRunDoctor:
    @patch("certisigma_census.doctor.check_api_connectivity")
    @patch("certisigma_census.doctor.check_api_key")
    def test_full_run(self, mock_key, mock_conn):
        mock_conn.return_value = MagicMock(name="api_health", status=STATUS_OK, message="ok", detail=None)
        mock_key.return_value = MagicMock(name="api_key", status=STATUS_OK, message="ok", detail=None)
        report = run_doctor()
        assert report.ok_count >= 3
        assert isinstance(report.all_ok, bool)

    @patch("certisigma_census.doctor.check_api_connectivity")
    @patch("certisigma_census.doctor.check_api_key")
    def test_report_counts(self, mock_key, mock_conn):
        mock_conn.return_value = MagicMock(name="api_health", status=STATUS_FAIL, message="fail", detail=None)
        mock_key.return_value = MagicMock(name="api_key", status=STATUS_WARN, message="warn", detail=None)
        report = run_doctor()
        assert report.fail_count >= 1
        assert report.warn_count >= 1
        assert not report.all_ok
