"""Per-run ABRA_DIR isolation (concurrency-restructure plan, cases 17-19). Real directories,
real symlinks, real git — abra itself is replaced by a recording stub where a CLI call is
involved (case 17), because these cases test OUR dir/env plumbing, not abra."""

from __future__ import annotations

import os
import stat
import subprocess
import sys

sys.path.insert(0, os.path.dirname(__file__))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
import run_recipe_ci  # noqa: E402
from concutil import wait_marker  # noqa: E402
from harness import abra  # noqa: E402

RECIPE = "fakerecipe"


def _git(cwd, *args):
    subprocess.run(
        ["git", "-c", "user.email=t@t", "-c", "user.name=t", *args],
        cwd=cwd,
        check=True,
        capture_output=True,
    )


def _make_fake_home(tmp_path):
    """A fake $HOME with a canonical ~/.abra: servers/default + catalogue dirs, and a recipe git
    repo with two tags whose data.txt differs (v1 -> 'one', v2 -> 'two', HEAD at v2)."""
    home = tmp_path / "home"
    (home / ".abra" / "servers" / "default").mkdir(parents=True)
    (home / ".abra" / "catalogue").mkdir(parents=True)
    repo = home / ".abra" / "recipes" / RECIPE
    repo.mkdir(parents=True)
    _git(repo, "init", "-q")
    (repo / "data.txt").write_text("one\n")
    _git(repo, "add", "data.txt")
    _git(repo, "commit", "-qm", "v1")
    _git(repo, "tag", "v1")
    (repo / "data.txt").write_text("two\n")
    _git(repo, "add", "data.txt")
    _git(repo, "commit", "-qm", "v2")
    _git(repo, "tag", "v2")
    return home


def test_17_per_run_dir_built_and_exported_before_abra(tmp_path, monkeypatch):
    """Case 17: setup_run_abra_dir builds the per-run dir correctly (servers/catalogue symlinks
    resolve to the canonical tree, recipes/ empty + writable) and $ABRA_DIR is exported before
    the first abra call — proven by a stub `abra` on PATH that records the env it saw."""
    home = _make_fake_home(tmp_path)
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.setenv("CCCI_RUNS_DIR", str(tmp_path / "runs"))
    monkeypatch.setenv("DRONE_BUILD_NUMBER", "777")
    monkeypatch.setenv("ABRA_DIR", "sentinel-to-be-overwritten")  # so monkeypatch restores it

    d = run_recipe_ci.setup_run_abra_dir()
    assert d == str(tmp_path / "runs" / "777" / "abra")
    assert os.environ["ABRA_DIR"] == d
    assert os.readlink(os.path.join(d, "servers")) == str(home / ".abra" / "servers")
    assert os.readlink(os.path.join(d, "catalogue")) == str(home / ".abra" / "catalogue")
    # symlinks RESOLVE (targets exist) and recipes/ is empty + writable
    assert os.path.isdir(os.path.join(d, "servers", "default"))
    assert os.path.isdir(os.path.join(d, "catalogue"))
    assert os.listdir(os.path.join(d, "recipes")) == []
    probe = os.path.join(d, "recipes", ".write-probe")
    open(probe, "w").close()
    os.remove(probe)
    # idempotent re-entry (Drone build-number retry): must not raise on existing symlinks
    assert run_recipe_ci.setup_run_abra_dir() == d

    # stub abra records $ABRA_DIR at call time; fetch_recipe's catalogue branch invokes it
    stub_dir = tmp_path / "bin"
    stub_dir.mkdir()
    log = tmp_path / "abra-env.log"
    stub = stub_dir / "abra"
    stub.write_text(f'#!/bin/sh\necho "$ABRA_DIR" >> {log}\nexit 0\n')
    stub.chmod(stub.stat().st_mode | stat.S_IEXEC)
    monkeypatch.setenv("PATH", f"{stub_dir}{os.pathsep}{os.environ['PATH']}")
    monkeypatch.delenv("CCCI_SKIP_FETCH", raising=False)
    run_recipe_ci.fetch_recipe(RECIPE, None, None)
    assert log.read_text().strip() == d, "abra was called without the per-run ABRA_DIR exported"


def test_18_concurrent_same_recipe_fetch_no_cross_talk(tmp_path, monkeypatch, pool):
    """Case 18: two CONCURRENT fetch+checkout flows of the SAME recipe into different ABRA_DIRs
    produce two correct, divergent trees (v1 vs v2) — the old shared-tree corruption scenario,
    now structurally safe with no lock. The canonical staged clone is untouched."""
    home = _make_fake_home(tmp_path)
    canonical_repo = home / ".abra" / "recipes" / RECIPE
    head_before = subprocess.run(
        ["git", "-C", canonical_repo, "rev-parse", "HEAD"], capture_output=True, text=True
    ).stdout.strip()

    runs = {}
    for name, ref in (("runA", "v1"), ("runB", "v2")):
        abra_dir = tmp_path / name / "abra"
        abra_dir.mkdir(parents=True)
        _, out = pool.spawn(
            "fetch-checkout",
            RECIPE,
            ref,
            env_extra={
                "HOME": str(home),
                "ABRA_DIR": str(abra_dir),
                "CCCI_SKIP_FETCH": "1",
            },
        )
        runs[name] = (out, ref, abra_dir)

    expect = {"v1": "one", "v2": "two"}
    for name, (out, ref, abra_dir) in runs.items():
        line = wait_marker(out, "RESULT", timeout=30)
        assert line, f"{name} never produced a RESULT"
        _, head, content = line.split()
        assert content == expect[ref], f"{name}@{ref}: tree content {content!r}"
        tree = abra_dir / "recipes" / RECIPE
        assert (tree / "data.txt").read_text().strip() == expect[ref]
        assert (
            head
            == subprocess.run(
                ["git", "-C", tree, "rev-parse", "HEAD"], capture_output=True, text=True
            ).stdout.strip()
        )

    # the two trees genuinely diverge AND the canonical staged clone is untouched
    a = (runs["runA"][2] / "recipes" / RECIPE / "data.txt").read_text()
    b = (runs["runB"][2] / "recipes" / RECIPE / "data.txt").read_text()
    assert a != b
    head_after = subprocess.run(
        ["git", "-C", canonical_repo, "rev-parse", "HEAD"], capture_output=True, text=True
    ).stdout.strip()
    assert head_after == head_before, "canonical clone must not be touched by per-run fetches"


def test_19_env_written_through_servers_symlink_lands_canonical(tmp_path, monkeypatch):
    """Case 19: an app .env written through the per-run servers/ symlink (what abra does under
    $ABRA_DIR) lands in the CANONICAL shared path — so janitor discovery and every
    expanduser('~/.abra/servers/...') reader keep working unchanged."""
    home = _make_fake_home(tmp_path)
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.setenv("CCCI_RUNS_DIR", str(tmp_path / "runs"))
    monkeypatch.setenv("DRONE_BUILD_NUMBER", "778")
    monkeypatch.setenv("ABRA_DIR", "sentinel-to-be-overwritten")
    d = run_recipe_ci.setup_run_abra_dir()

    domain = "test-abc123.ci.commoninternet.net"
    via_symlink = os.path.join(d, "servers", "default", f"{domain}.env")
    with open(via_symlink, "w") as f:
        f.write("TYPE=fakerecipe:1.0.0\nDOMAIN=placeholder\n")

    canonical = home / ".abra" / "servers" / "default" / f"{domain}.env"
    assert canonical.is_file(), ".env written via the symlink must land in the canonical path"
    # the canonical-path readers/writers (abra.env_get/env_set use ~/.abra) see the same file
    assert abra.env_get(domain, "TYPE") == "fakerecipe:1.0.0"
    abra.env_set(domain, "DOMAIN", domain)
    with open(via_symlink) as f:
        assert f"DOMAIN={domain}" in f.read()


def test_18b_run_id_manual_fallback_is_per_process(tmp_path, monkeypatch):
    """Companion to case 18: two concurrent MANUAL runs (no DRONE_BUILD_NUMBER) must not share an
    abra dir either — the manual fallback is pid-suffixed."""
    home = _make_fake_home(tmp_path)
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.setenv("CCCI_RUNS_DIR", str(tmp_path / "runs"))
    monkeypatch.delenv("DRONE_BUILD_NUMBER", raising=False)
    monkeypatch.delenv("CCCI_APP_DOMAIN", raising=False)
    monkeypatch.delenv("CCCI_RUN_ID", raising=False)
    monkeypatch.setenv("ABRA_DIR", "sentinel-to-be-overwritten")
    d = run_recipe_ci.setup_run_abra_dir()
    assert f"manual-{os.getpid()}" in d
