"""Unit tests for the customization manifest (rcust P5; spec §8 R4 mitigation).

The manifest is PURE PRESENTATION (must never influence a verdict); these tests pin that it is
COMPLETE (every customization surface a synthetic recipe exercises shows up), DETERMINISTIC
(same inputs -> byte-identical JSON), serializable, and HC2-honoring (unapproved repo-local
contributions are invisible). Pure / tmp-file only. Run cold:
  cc-ci-run -m pytest tests/unit/test_manifest.py -q
"""

from __future__ import annotations

import json
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import discovery, manifest  # noqa: E402
from harness import meta as meta_mod  # noqa: E402

RECIPE = "ccci-manifest-fixture"


def _mk_synthetic(tmp_path, monkeypatch, approved=True):
    """A synthetic recipe dir exercising EVERY manifest surface, plus a repo-local tests dir.

    cc-ci side: meta (2 data keys + 1 hook key non-default), ops.py (2 pre-ops), install_steps.sh,
    compose.ccci.yml, test_backup.py overlay, 3 custom tests.
    repo-local side: test_restore.py overlay + 1 custom test (visible iff approved, HC2).
    """
    ccci_root = tmp_path / "cc-ci-tests"
    d = ccci_root / RECIPE
    (d / "custom").mkdir(parents=True)
    (d / "recipe_meta.py").write_text(
        "HTTP_TIMEOUT = 600\n"
        "DEPS = ['keycloak']\n"
        "def EXTRA_ENV(ctx):\n    return {}\n"
        "_PRIVATE = 'exempt'\n"
    )
    (d / "ops.py").write_text("def pre_upgrade(ctx):\n    pass\n\ndef pre_backup(ctx):\n    pass\n")
    (d / "install_steps.sh").write_text("#!/usr/bin/env bash\n")
    (d / "compose.ccci.yml").write_text("version: '3.8'\n")
    (d / "test_backup.py").write_text("# lifecycle overlay\n")
    (d / "custom" / "test_a.py").write_text("# custom\n")
    (d / "custom" / "test_b.py").write_text("# custom\n")
    (d / "custom" / "test_ui.py").write_text("# custom\n")

    rl = tmp_path / "repo-local"
    (rl / "custom").mkdir(parents=True)
    (rl / "custom" / "test_c.py").write_text("# repo-local custom\n")
    (rl / "test_restore.py").write_text("# repo-local lifecycle overlay\n")

    monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(ccci_root / r))
    monkeypatch.setattr(meta_mod, "TESTS_DIR", str(ccci_root))
    approved_file = tmp_path / "approved.txt"
    approved_file.write_text(f"{RECIPE}\n" if approved else "")
    monkeypatch.setenv("CCCI_REPO_LOCAL_APPROVED_FILE", str(approved_file))

    meta = meta_mod.load(RECIPE, tests_dir=str(ccci_root))
    return meta, str(rl)


def test_manifest_complete(tmp_path, monkeypatch):
    meta, rl = _mk_synthetic(tmp_path, monkeypatch)
    m = manifest.build(RECIPE, meta, rl)
    assert m["meta_non_default"] == {
        "DEPS": ["keycloak"],
        "EXTRA_ENV": "<hook>",
        "HTTP_TIMEOUT": 600,
    }
    assert m["hooks"] == {
        "ops.py": {"cc-ci": ["pre_backup", "pre_upgrade"]},
        "install_steps.sh": "cc-ci",
        "compose.ccci.yml": "cc-ci",
    }
    assert m["overlays"] == {"backup": "cc-ci", "restore": "repo-local"}
    assert m["custom_tests"] == {
        "cc-ci": {"custom": 3},
        "repo-local": {"custom": 1},
    }
    assert m["env_overrides"] == []


def test_manifest_deterministic_and_serializable(tmp_path, monkeypatch):
    meta, rl = _mk_synthetic(tmp_path, monkeypatch)
    a = manifest.build(RECIPE, meta, rl)
    b = manifest.build(RECIPE, meta, rl)
    assert json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
    assert json.loads(json.dumps(a)) == a


def test_manifest_zero_config_floor(tmp_path, monkeypatch):
    ccci_root = tmp_path / "cc-ci-tests"
    (ccci_root / RECIPE).mkdir(parents=True)
    monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(ccci_root / r))
    monkeypatch.setattr(meta_mod, "TESTS_DIR", str(ccci_root))
    monkeypatch.setenv("CCCI_REPO_LOCAL_APPROVED_FILE", str(tmp_path / "missing.txt"))
    meta = meta_mod.load(RECIPE, tests_dir=str(ccci_root))
    m = manifest.build(RECIPE, meta, None)
    assert m == {
        "meta_non_default": {},
        "hooks": {},
        "overlays": {},
        "custom_tests": {},
        "env_overrides": [],
    }
    out = manifest.render(RECIPE, m)
    assert f"===== customization manifest: {RECIPE} =====" in out
    assert "(none — zero-config floor)" in out


def test_manifest_repo_local_hc2_gate(tmp_path, monkeypatch):
    meta, rl = _mk_synthetic(tmp_path, monkeypatch, approved=False)
    m = manifest.build(RECIPE, meta, rl)
    assert m["overlays"] == {"backup": "cc-ci"}
    assert "repo-local" not in m["custom_tests"]


def test_manifest_env_overrides_and_ci_flag(tmp_path, monkeypatch):
    meta, rl = _mk_synthetic(tmp_path, monkeypatch)
    monkeypatch.setenv("CCCI_SKIP_GENERIC_BACKUP", "1")
    monkeypatch.setenv("CCCI_SKIP_GENERIC_UPGRADE", "0")
    m = manifest.build(RECIPE, meta, rl)
    assert m["env_overrides"] == ["CCCI_SKIP_GENERIC_BACKUP"]
    monkeypatch.delenv("DRONE", raising=False)
    assert "!!" not in manifest.render(RECIPE, m)
    monkeypatch.setenv("DRONE", "true")
    assert "!! dev-only override active in CI" in manifest.render(RECIPE, m)


def test_manifest_redacts_sensitive_named_values(tmp_path, monkeypatch):
    ccci_root = tmp_path / "cc-ci-tests"
    d = ccci_root / RECIPE
    d.mkdir(parents=True)
    (d / "recipe_meta.py").write_text(
        "EXTRA_ENV = {\n"
        "    'SECRET_KEY_BASE': 'dummy-ci-constant',\n"
        "    'API_KEY': 'also-dummy',\n"
        "    'KEYCLOAK_URL': 'https://kc.example',\n"
        "}\n"
    )
    monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(ccci_root / r))
    monkeypatch.setattr(meta_mod, "TESTS_DIR", str(ccci_root))
    monkeypatch.setenv("CCCI_REPO_LOCAL_APPROVED_FILE", str(tmp_path / "missing.txt"))
    meta = meta_mod.load(RECIPE, tests_dir=str(ccci_root))
    m = manifest.build(RECIPE, meta, None)
    assert m["meta_non_default"]["EXTRA_ENV"] == {
        "SECRET_KEY_BASE": "<redacted>",
        "API_KEY": "<redacted>",
        "KEYCLOAK_URL": "https://kc.example",
    }
    out = manifest.render(RECIPE, m)
    assert "dummy-ci-constant" not in out and "also-dummy" not in out
    assert "SECRET_KEY_BASE" in out


def test_render_lists_every_surface(tmp_path, monkeypatch):
    meta, rl = _mk_synthetic(tmp_path, monkeypatch)
    out = manifest.render(RECIPE, manifest.build(RECIPE, meta, rl))
    lines = out.splitlines()
    assert lines[0] == f"===== customization manifest: {RECIPE} ====="
    assert "meta (non-default): DEPS=['keycloak'] EXTRA_ENV='<hook>' HTTP_TIMEOUT=600" in lines
    assert (
        "hooks: ops.py[pre_backup,pre_upgrade](cc-ci) install_steps.sh(cc-ci) compose.ccci.yml(cc-ci)"
        in lines
    )
    assert "overlays: test_backup.py(cc-ci) test_restore.py(repo-local)" in lines
    assert "custom tests: custom/=3 (cc-ci) custom/=1 (repo-local)" in lines
    assert "env overrides: (none)" in lines
