"""Failure-time recovery and the Core-owned manifest, run for real (node + a real bare remote).

When the provider dies (or a later step fails) after changing files but before the normal
checkpoint, the runner must keep that work on the run branch so a retry builds on it. Only what a
normal commit would publish is kept (same allowlist and leak scan), preview is never touched, and the
recovery itself can never fail the run a second time.

feature-packs.json starts from Core's projection on every fresh attempt, but an attempt that
continues the same run on its work branch keeps the run's own manifest (it is what must be validated
or corrected)."""

from __future__ import annotations

import json
import os
import subprocess

import pytest
from tests.test_runner_work_branch import (  # noqa: F401  (repo is a fixture)
    BRANCH,
    GIT,
    NODE,
    RUN_ID,
    agent_commit,
    git,
    repo,
    run_node,
    write,
)

pytestmark = pytest.mark.skipif(not (NODE and GIT), reason="node and git are required")


def recover(repo, *, checkpoint="null", activate=True):
    """Run saveWorkOnFailure on the repo's working clone; returns what the runner would report."""
    active = (
        f"activeWork = {{ branch: {json.dumps(BRANCH)}, checkpoint: {checkpoint}, sha: null,"
        f" changesBase: {json.dumps(repo.base)} }};"
        if activate
        else "activeWork = null;"
    )
    return run_node(repo, f"""
      {active}
      await saveWorkOnFailure(payload);
      console.log('RESULT ' + JSON.stringify({{ fields: workFields() }}));
    """)["fields"]


def on_origin(repo, path):
    return repo.origin_has(BRANCH, path)


def branch_exists(repo):
    return git(repo.origin, "rev-parse", "--verify", f"refs/heads/{BRANCH}", check=False) != ""


# ---- saving the work ----------------------------------------------------------------------------


def test_work_the_agent_committed_is_saved_when_the_run_fails_before_its_checkpoint(repo):
    agent_commit(repo)
    fields = recover(repo)
    assert fields["checkpoint"] == "partial_work"
    assert fields["checkpoint_sha"] == repo.origin_rev(BRANCH) == git(repo.work, "rev-parse", "HEAD")
    assert on_origin(repo, "index.html")
    assert repo.origin_rev("preview") == repo.base  # preview is never touched


def test_files_the_agent_left_uncommitted_are_saved_too(repo):
    write(os.path.join(repo.work, "src", "App.vue"), "<template><h1>site</h1></template>\n")
    fields = recover(repo)
    assert fields["checkpoint"] == "partial_work"
    assert on_origin(repo, "src/App.vue")
    message = git(repo.origin, "log", "-1", "--format=%B", BRANCH)
    assert "Authored by P2HS only" in message  # same authorship rule as a normal commit


def test_only_what_a_normal_commit_would_publish_is_saved(repo):
    write(os.path.join(repo.work, "src", "App.vue"), "<template/>\n")
    write(os.path.join(repo.work, ".env"), "SECRET=1\n")
    write(os.path.join(repo.work, ".cursorrules"), "system prompt\n")
    write(os.path.join(repo.work, ".p2hs", "ai-context", "agent-prompt.md"), "the prompt\n")
    write(os.path.join(repo.work, "config", "service.key"), "material\n")
    recover(repo)
    assert on_origin(repo, "src/App.vue")
    for path in (".env", ".cursorrules", ".p2hs/ai-context/agent-prompt.md", "config/service.key"):
        assert not on_origin(repo, path), path


def test_a_leak_means_the_uncommitted_files_are_left_out_not_that_recovery_fails(repo):
    agent_commit(repo)  # already committed by the agent: safe to keep
    write(os.path.join(repo.work, "src", "leaky.js"), "const k = `-----BEGIN PRIVATE KEY-----`;\n")
    write(os.path.join(repo.work, "src", "fine.vue"), "<template/>\n")
    fields = recover(repo)
    assert fields["checkpoint"] == "partial_work"  # the committed work is still saved
    assert on_origin(repo, "index.html")
    assert not on_origin(repo, "src/leaky.js") and not on_origin(repo, "src/fine.vue")
    assert git(repo.work, "diff", "--cached", "--name-only") == ""  # nothing left staged


def test_the_manifests_the_gate_needs_travel_with_the_saved_work(repo):
    agent_commit(repo)
    write(os.path.join(repo.work, "p2hs", "feature-packs.json"), '{"compose": []}\n')  # git-ignored
    recover(repo)
    assert on_origin(repo, "p2hs/feature-packs.json")


# ---- when there is nothing to save, or it must not run ---------------------------------------------


def test_a_run_that_changed_nothing_saves_nothing(repo):
    fields = recover(repo)
    assert fields.get("checkpoint") is None
    assert not branch_exists(repo)  # no empty "work in progress" branch, no misleading work_saved


def test_a_run_that_already_has_a_checkpoint_is_left_alone(repo):
    agent_commit(repo)
    fields = recover(repo, checkpoint="'agent_output'")
    assert fields["checkpoint"] == "agent_output"
    assert not branch_exists(repo)  # recovery does not re-save or downgrade a real checkpoint


def test_without_a_work_branch_nothing_changes(repo):
    agent_commit(repo)
    assert recover(repo, activate=False) == {}
    assert not branch_exists(repo)


def test_a_rejected_push_is_reported_as_not_saved_and_never_throws_or_forces(repo):
    agent_commit(repo)
    git(repo.other, "fetch", "-q", "origin")
    git(repo.other, "checkout", "-q", "-b", "elsewhere", "origin/preview")
    write(os.path.join(repo.other, "intruder.txt"), "x\n")
    git(repo.other, "add", "-A")
    git(repo.other, "commit", "-q", "-m", "intruder")
    git(repo.other, "push", "-q", "origin", f"HEAD:refs/heads/{BRANCH}")
    intruder = repo.origin_rev(BRANCH)
    fields = recover(repo)
    assert fields.get("checkpoint") is None
    assert repo.origin_rev(BRANCH) == intruder  # not overwritten


# ---- the Core-owned manifest --------------------------------------------------------------------

CORE = {"version": 1, "install": [{"pack_slug": "core-pack", "config": {}}]}
AGENT = {
    "version": 1,
    "install": [{"pack_slug": "agent-pack", "config": {}}],
    "compose": [{"name": "contact_submission"}],
}


def materialize(repo, *, existing, extra_payload="", branch=True, file_text=None):
    """Run materializeCoreComposition with an existing manifest; returns the resulting file."""
    text = file_text if file_text is not None else json.dumps(existing)
    work_branch = f"work_branch_name: {json.dumps(BRANCH)}," if branch else ""
    return run_node(repo, f"""
      await mkdir('p2hs', {{ recursive: true }});
      await writeFile('p2hs/feature-packs.json', {json.dumps(text)}, 'utf8');
      const attempt = {{ ...payload, {work_branch} {extra_payload}
        context: {{ core_composition_files: {{
          'p2hs/feature-packs.json': {json.dumps(json.dumps(CORE))},
          'p2hs/app-blocks-plan.json': '{{}}',
        }} }} }};
      await materializeCoreComposition(attempt);
      const written = JSON.parse(await readFile('p2hs/feature-packs.json', 'utf8'));
      console.log('RESULT ' + JSON.stringify(written));
    """)


def test_a_fresh_attempt_starts_from_core_alone_and_never_inherits_a_stale_manifest(repo):
    assert materialize(repo, existing=AGENT) == CORE


def test_a_resumed_attempt_keeps_the_runs_own_manifest_and_adds_what_core_requires(repo):
    written = materialize(repo, existing=AGENT, extra_payload="resume: { skip_agent: true },")
    assert written["compose"] == [{"name": "contact_submission"}]  # the agent's intent survives
    slugs = {row["pack_slug"] for row in written["install"]}
    assert slugs == {"agent-pack", "core-pack"}  # and Core's required install is still guaranteed


def test_a_correction_attempt_keeps_the_manifest_the_agent_has_to_fix(repo):
    written = materialize(
        repo, existing=AGENT, extra_payload="resume: { skip_agent: false, last_checkpoint: 'agent_output' },"
    )
    assert written["compose"] == [{"name": "contact_submission"}]


def test_a_time_budget_continuation_keeps_the_manifest_written_so_far(repo):
    written = materialize(repo, existing=AGENT, extra_payload="continuation_attempt: 1,")
    assert written["compose"] == [{"name": "contact_submission"}]


def test_without_a_work_branch_the_manifest_is_always_replaced(repo):
    written = materialize(repo, existing=AGENT, extra_payload="resume: { skip_agent: true },", branch=False)
    assert written == CORE


def test_an_unreadable_manifest_is_replaced_rather_than_kept(repo):
    written = materialize(
        repo, existing=None, file_text="{not json", extra_payload="resume: { skip_agent: true },"
    )
    assert written == CORE
    written = materialize(repo, existing=None, file_text="[1, 2]", extra_payload="resume: { skip_agent: true },")
    assert written == CORE


def test_the_runner_wires_recovery_into_the_failure_handler():
    from src.domain.codegen.actions_pipeline import build_codegen_actions_runner

    runner = build_codegen_actions_runner()
    handler = runner[runner.index("main().catch(async (error) => {"):]
    assert handler.index("await saveWorkOnFailure(") < handler.index('status: "failed"')
    assert RUN_ID  # (imported for the parametrised paths above)
    assert subprocess.run([NODE, "--version"], capture_output=True).returncode == 0
