import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import postgres from "postgres";
import { approvedJourneyEnvironment, requiredEnvironment } from "./exact-journey.js";
import {
  authoritativeRemoteHead,
  canonicalGitHubRepository,
  commandOutput,
} from "./repository-identity.js";

const databaseUrl = requiredEnvironment(process.env, "AWP_TEST_POSTGRES_URL");
const repositoryUrl = requiredEnvironment(process.env, "AWP_REPOSITORY_REMOTE_URL");
const repositoryRoot = requiredEnvironment(process.env, "AWP_REPOSITORY_CHECKOUT_PATH");
const defaultBranch = requiredEnvironment(process.env, "AWP_REPOSITORY_DEFAULT_BRANCH");
const timeoutMs = Number(process.env.AWP_GOLIVE_TIMEOUT_MS ?? "120000");
const { projectId, planRevisionId, factoryRunId, firstTaskId } = approvedJourneyEnvironment();

function git(args: string[], options: { env?: NodeJS.ProcessEnv; input?: string } = {}) {
  return spawnSync("git", args, { cwd: repositoryRoot, encoding: "utf8", ...options });
}

const origin = git(["remote", "get-url", "origin"]);
assert.equal(origin.status, 0, `checkout origin is unavailable: ${origin.stderr}`);
const originUrl = commandOutput(origin.stdout);
assert.equal(
  canonicalGitHubRepository(originUrl),
  canonicalGitHubRepository(repositoryUrl),
  "checkout origin must match the manifest repository remote URL",
);
const remoteHead = git(["ls-remote", "--symref", "origin", "HEAD"]);
assert.equal(
  remoteHead.status,
  0,
  `remote origin default branch is unavailable: ${remoteHead.stderr}`,
);
const authoritativeHead = authoritativeRemoteHead(remoteHead.stdout);
assert.equal(authoritativeHead.branchRef, `refs/heads/${defaultBranch}`);

type CandidateRow = {
  change_set_id: string;
  task_id: string;
  base_identity: string;
  candidate_digest: string;
  candidate_manifest: {
    treeDigest: string;
    patchDigest: string;
    changedPaths: string[];
    changes: { path: string; kind: "add" | "modify" | "delete" }[];
  };
  diff: string;
  agent_run_task_id: string;
  provider_id: string | null;
  account_id: string | null;
  model: string | null;
  attempt_status: string;
  agent_run_status: string;
};

const sql = postgres(databaseUrl, { max: 1 });
try {
  const deadline = Date.now() + timeoutMs;
  let candidates: CandidateRow[] = [];
  while (Date.now() < deadline) {
    candidates = await sql<CandidateRow[]>`
      select
        cs.id as change_set_id,
        cs.task_id,
        cs.base_identity,
        cs.candidate_digest,
        cs.candidate_manifest,
        cs.diff,
        ar.task_id as agent_run_task_id,
        a.provider_id,
        a.account_id,
        a.model,
        a.status as attempt_status,
        ar.status as agent_run_status
      from change_sets cs
      join attempts a on a.id = cs.producer_attempt_id
      join agent_runs ar on ar.id = a.agent_run_id
      join tasks t on t.id = cs.task_id and t.project_id = cs.project_id
      join projects p on p.id = cs.project_id
      where cs.project_id = ${projectId}
        and p.repository_url = ${repositoryUrl}
        and cs.task_id = ${firstTaskId}
        and t.plan_revision_id = ${planRevisionId}
        and ar.factory_run_id = ${factoryRunId}
        and ar.task_id = ${firstTaskId}
      order by cs.created_at, cs.id
    `;
    if (candidates.length > 0) break;
    await delay(100);
  }
  assert.equal(candidates.length, 1, "the exact first Task must produce exactly one ChangeSet");
  const candidate = candidates[0]!;
  assert.equal(candidate.task_id, firstTaskId);
  assert.ok(candidate.base_identity.trim());
  assert.ok(candidate.candidate_digest.trim());
  assert.ok(candidate.candidate_manifest);
  assert.ok(candidate.diff.trim(), "the ChangeSet diff must be non-empty");
  assert.equal(candidate.provider_id, "provider:acp");
  assert.ok(candidate.account_id);
  assert.ok(candidate.model);
  assert.equal(candidate.attempt_status, "terminal");
  assert.equal(candidate.agent_run_status, "completed");
  assert.equal(candidate.agent_run_task_id, firstTaskId);

  assert.ok(Array.isArray(candidate.candidate_manifest.changedPaths));
  assert.ok(Array.isArray(candidate.candidate_manifest.changes));
  assert.ok(candidate.candidate_manifest.changedPaths.length > 0);
  assert.equal(
    new Set(candidate.candidate_manifest.changedPaths).size,
    candidate.candidate_manifest.changedPaths.length,
  );
  assert.equal(
    new Set(candidate.candidate_manifest.changes.map(({ path }) => path)).size,
    candidate.candidate_manifest.changes.length,
  );
  assert.equal(candidate.candidate_manifest.treeDigest, candidate.candidate_digest);
  assert.equal(
    candidate.candidate_manifest.patchDigest,
    createHash("sha256").update(candidate.diff).digest("hex"),
  );

  assert.match(candidate.base_identity, /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u);
  assert.equal(
    candidate.base_identity,
    authoritativeHead.oid,
    "first candidate base must equal authoritative remote HEAD before trusted merge",
  );

  const indexDirectory = mkdtempSync(join(tmpdir(), "awp-golive-candidate-"));
  const indexFile = join(indexDirectory, "index");
  const gitEnv = { ...process.env, GIT_INDEX_FILE: indexFile };
  try {
    const base = git(["rev-parse", "--verify", `${candidate.base_identity}^{commit}`], {
      env: gitEnv,
    });
    assert.equal(base.status, 0, base.stderr);
    assert.equal(base.stdout.trim(), candidate.base_identity);

    const readTree = git(["read-tree", candidate.base_identity], { env: gitEnv });
    assert.equal(readTree.status, 0, readTree.stderr);
    const apply = git(["apply", "--cached", "-"], { env: gitEnv, input: candidate.diff });
    assert.equal(apply.status, 0, apply.stderr);

    const changed = git(
      ["diff", "--cached", "--name-status", "--no-renames", "-z", candidate.base_identity],
      { env: gitEnv },
    );
    assert.equal(changed.status, 0, changed.stderr);
    const fields = changed.stdout.split("\0").filter(Boolean);
    assert.equal(fields.length % 2, 0);
    const kindByStatus = { A: "add", M: "modify", T: "modify", D: "delete" } as const;
    const reconstructedChanges = Array.from({ length: fields.length / 2 }, (_, index) => {
      const status = fields[index * 2];
      const path = fields[index * 2 + 1];
      assert.ok(status === "A" || status === "M" || status === "T" || status === "D");
      assert.ok(path);
      return { path, kind: kindByStatus[status] };
    }).sort((left, right) => left.path.localeCompare(right.path));
    assert.deepEqual(
      [...candidate.candidate_manifest.changedPaths].sort(),
      reconstructedChanges.map(({ path }) => path).sort(),
    );
    assert.deepEqual(
      [...candidate.candidate_manifest.changes].sort((left, right) =>
        left.path.localeCompare(right.path),
      ),
      reconstructedChanges,
    );

    const tree = git(["write-tree"], { env: gitEnv });
    assert.equal(tree.status, 0, tree.stderr);
    assert.equal(tree.stdout.trim(), candidate.candidate_digest);
  } finally {
    rmSync(indexDirectory, { recursive: true, force: true });
  }

  process.stdout.write(
    `AC-15..AC-16 PASS: exact FactoryRun ${factoryRunId} first Task ${firstTaskId} produced ChangeSet ${candidate.change_set_id}, reconstructing its immutable candidate tree from authoritative base and durable ACP Attempt provenance\n`,
  );
} finally {
  await sql.end({ timeout: 5 });
}
