"""Shared pytest fixtures for recipe CI (plan §4.3).

A run is parameterized by env: RECIPE, REF (PR head sha), PR, SRC (head repo). The harness
computes a unique app domain per run so concurrent runs never collide, and GUARANTEES teardown
(undeploy + volume + secret removal) via a finalizer, even on failure.
"""

from __future__ import annotations

import os
import sys

import pytest

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


@pytest.fixture(scope="session")
def recipe() -> str:
    return os.environ.get("RECIPE", "custom-html")


@pytest.fixture(scope="session")
def meta(recipe):
    """The recipe's FULL validated customization (RecipeMeta, attribute access) via the single
    loader (rcust P1 — previously this fixture saw only the 4 base keys, spec §8 R3)."""
    return meta_mod.load(recipe)


@pytest.fixture(scope="session")
def live_app() -> str:
    """Phase 1d shared-deployment contract: the orchestrator deploys ONCE and runs each tier
    (generic or overlay) as its own pytest invocation against that single live deployment, passing
    its domain in CCCI_APP_DOMAIN. Tiers are assertion-only (and lifecycle ops mutate in place) —
    they NEVER deploy or tear down. This guarantees one deploy + one teardown per run (DG4.1)."""
    domain = os.environ.get("CCCI_APP_DOMAIN")
    assert domain, "CCCI_APP_DOMAIN not set — a tier must run under the deploy-once orchestrator"
    return domain


@pytest.fixture
def op_state() -> dict:
    """The orchestrator's run-scoped op context (rcust P4): versions, artifact paths — written to
    `$CCCI_OP_STATE_FILE` after each lifecycle op (e.g. `{"upgrade": {"before": {...},
    "head_ref": ...}, "backup": {"snapshot_id": ...}}`). Overlay tests read op facts from here
    instead of hand-parsing env/JSON. Skips with a clear reason outside an orchestrator run."""
    import json

    path = os.environ.get("CCCI_OP_STATE_FILE")
    if not path:
        pytest.skip(
            "CCCI_OP_STATE_FILE not set — op_state is only available under the orchestrator"
        )
    if not os.path.exists(path):
        pytest.skip(f"op-state file missing ({path}) — orchestrator has not performed an op yet")
    try:
        with open(path) as f:
            return json.load(f)
    except ValueError:
        pytest.skip(f"op-state file unreadable/not JSON ({path})")


class _DepEntry(dict):
    """One provisioned dep (full creds dict) with attribute sugar: `entry.domain`, `entry.realm`,
    `entry.client_secret`, ... — dict-style access works too (rcust P2d)."""

    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError as e:
            raise AttributeError(name) from e


@pytest.fixture(scope="session")
def deps() -> dict[str, _DepEntry]:
    """The recipe's provisioned deps (rcust P2d — consolidates the old `deps_apps`+`deps_creds`
    pair). When a recipe declares `DEPS = [...]` in its `recipe_meta.py`, the orchestrator
    provisions each dep BEFORE the single deploy and persists per-run identity + SSO creds to
    `$CCCI_DEPS_FILE`. `deps["keycloak"]` carries domain/realm/client_id/client_secret/user/
    password/email/admin_user/admin_password/discovery_url/token_url/... (`.domain` etc. work as
    attributes). Empty when no deps declared OR deps-not-ready — pair with
    `@pytest.mark.requires_deps` so the F2-11 skip-report keeps the green signal honest."""
    state = deps_mod.deps_as_dict(deps_mod.load_run_state())
    return {r: _DepEntry(e) for r, e in state.items()}


def pytest_collection_modifyitems(config, items):
    """SSO-dep plan §4: tests marked `@pytest.mark.requires_deps` are skipped with reason
    `deps-not-ready: <captured-err>` when the orchestrator's dep provisioning failed
    (orchestrator sets CCCI_DEPS_READY=0 in env). Non-deps custom tests are unaffected.

    This is failure-isolation per plan §1 — generic tiers cannot break the SSO-marked tests'
    skip status, and an SSO-setup failure cannot break the generic tiers (they run first)."""
    deps_ready_env = os.environ.get("CCCI_DEPS_READY", "1")
    if deps_ready_env == "1":
        return
    reason = os.environ.get("CCCI_DEPS_NOT_READY_REASON", "(no reason given)")
    skip_mark = pytest.mark.skip(reason=f"deps-not-ready: {reason}")
    skipped = 0
    for item in items:
        if "requires_deps" in item.keywords:
            item.add_marker(skip_mark)
            skipped += 1
    # F2-11: a skip-only pytest file exits 0, so without this the orchestrator can't tell
    # "SSO verified" from "SSO test silently skipped because deps weren't ready". Record the count
    # of requires_deps tests we skipped to a report file the orchestrator reads — it surfaces the
    # count in RUN SUMMARY and FAILS the recipe's SSO claim (a green exit must not mask an unrun
    # SSO test). Appended one line per pytest invocation (one per custom file); orchestrator sums.
    report = os.environ.get("CCCI_DEPS_SKIP_REPORT")
    if report and skipped:
        try:
            with open(report, "a") as f:
                f.write(f"{skipped}\n")
        except OSError:
            pass


def pytest_configure(config):
    """Register the `requires_deps` marker so pytest doesn't warn about it."""
    config.addinivalue_line(
        "markers",
        "requires_deps: test requires DEPS-declared services + dep provisioning success.",
    )
