import { afterEach, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { publishPlatformInfraGiteaGitOps } from "./publish-platform-infra-gitea-gitops.mjs";

const roots: string[] = [];

afterEach(() => {
  while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true });
});

test("independent desired publisher is idempotent and preserves unrelated GitOps files", () => {
  const fixture = createFixture();
  checkout(fixture.sourceWork, fixture.commits.second);
  const first = publish(fixture, fixture.commits.second);
  expect(first).toMatchObject({ ok: true, status: "published", changed: true, pushAttempts: 1 });
  expect(show(fixture.gitOpsBare, fixture.delivery.branch, "deploy/gitops/unidesk-host/todo-note.yaml")).toBe("existing todo\n");
  expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.desiredManifestPath)).toContain("kind: PersistentVolumeClaim");
  expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.bootstrapApplicationPath)).toContain("name: platform-infra-gitea-nc01");

  const second = publish(fixture, fixture.commits.second);
  expect(second).toMatchObject({ ok: true, status: "idempotent", changed: false, pushAttempts: 0 });
});

test("CAS retries from a fresh head and preserves a concurrent todo-note update", () => {
  const fixture = createFixture();
  checkout(fixture.sourceWork, fixture.commits.second);
  let raced = false;
  const result = publishPlatformInfraGiteaGitOps({
    delivery: fixture.delivery,
    desiredManifest: fixture.desiredManifest,
    sourceRoot: fixture.sourceWork,
    sourceCommit: fixture.commits.second,
    worktree: fixture.publisherWorktree,
    beforePush: ({ attempt }: { attempt: number }) => {
      if (attempt !== 1 || raced) return;
      raced = true;
      pushConcurrentFile(fixture.root, fixture.gitOpsBare, fixture.delivery.branch);
    },
  });
  expect(result).toMatchObject({ ok: true, status: "published", changed: true, pushAttempts: 2 });
  expect(show(fixture.gitOpsBare, fixture.delivery.branch, "deploy/gitops/unidesk-host/concurrent-todo.yaml")).toBe("concurrent todo\n");
  expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath)).toContain(fixture.commits.second);
});

test("out-of-order source is superseded and divergent history fails closed", () => {
  const fixture = createFixture();
  checkout(fixture.sourceWork, fixture.commits.second);
  expect(publish(fixture, fixture.commits.second)).toMatchObject({ status: "published" });

  checkout(fixture.sourceWork, fixture.commits.first);
  expect(publish(fixture, fixture.commits.first)).toMatchObject({
    ok: true,
    status: "superseded",
    authoritativeSourceCommit: fixture.commits.second,
    changed: false,
  });
  expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath)).toContain(fixture.commits.second);

  checkout(fixture.sourceWork, fixture.commits.diverged);
  expect(() => publish(fixture, fixture.commits.diverged)).toThrow(/diverges from authoritative GitOps source/);
  expect(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath)).toContain(fixture.commits.second);
});

test("same source commit with conflicting stored materialization hashes fails closed", () => {
  const fixture = createFixture();
  checkout(fixture.sourceWork, fixture.commits.second);
  expect(publish(fixture, fixture.commits.second)).toMatchObject({ status: "published" });
  mutateGitOpsState(fixture.root, fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath, (state) => ({
    ...state,
    desiredManifestSha256: "f".repeat(64),
  }));
  expect(() => publish(fixture, fixture.commits.second)).toThrow(/same source commit .* conflicting GitOps materialization state/);
  const state = JSON.parse(show(fixture.gitOpsBare, fixture.delivery.branch, fixture.delivery.releaseStatePath));
  expect(state.desiredManifestSha256).toBe("f".repeat(64));
});

function createFixture() {
  const root = mkdtempSync(join(tmpdir(), "unidesk-platform-infra-gitea-publish-"));
  roots.push(root);
  const sourceWork = join(root, "source-work");
  const sourceBare = join(root, "source.git");
  const gitOpsSeed = join(root, "gitops-seed");
  const gitOpsBare = join(root, "gitops.git");
  mkdirSync(sourceWork);
  git(["init", "-b", "master"], sourceWork);
  identity(sourceWork);
  write(sourceWork, "source.txt", "first\n");
  git(["add", "source.txt"], sourceWork);
  git(["commit", "-m", "first"], sourceWork);
  const first = git(["rev-parse", "HEAD"], sourceWork).stdout.trim();
  write(sourceWork, "source.txt", "second\n");
  git(["commit", "-am", "second"], sourceWork);
  const second = git(["rev-parse", "HEAD"], sourceWork).stdout.trim();
  git(["checkout", "-b", "diverged", first], sourceWork);
  write(sourceWork, "source.txt", "diverged\n");
  git(["commit", "-am", "diverged"], sourceWork);
  const diverged = git(["rev-parse", "HEAD"], sourceWork).stdout.trim();
  git(["checkout", "master"], sourceWork);
  git(["init", "--bare", sourceBare], root);
  git(["remote", "add", "origin", fileUrl(sourceBare)], sourceWork);
  git(["push", "origin", "master"], sourceWork);
  const prefix = "refs/unidesk/snapshots/test-unidesk-master";
  for (const commit of [first, second, diverged]) git(["push", "origin", `${commit}:${prefix}/${commit}`], sourceWork);

  mkdirSync(gitOpsSeed);
  git(["init", "-b", "unidesk-host-gitops"], gitOpsSeed);
  identity(gitOpsSeed);
  write(gitOpsSeed, "deploy/gitops/unidesk-host/todo-note.yaml", "existing todo\n");
  git(["add", "."], gitOpsSeed);
  git(["commit", "-m", "seed gitops"], gitOpsSeed);
  git(["init", "--bare", gitOpsBare], root);
  git(["remote", "add", "origin", fileUrl(gitOpsBare)], gitOpsSeed);
  git(["push", "origin", "unidesk-host-gitops"], gitOpsSeed);

  const delivery = {
    enabled: true,
    targetId: "NC01",
    readUrl: fileUrl(gitOpsBare),
    writeUrl: fileUrl(gitOpsBare),
    branch: "unidesk-host-gitops",
    sourceSnapshotPrefix: prefix,
    desiredManifestPath: "deploy/gitops/platform-infra/gitea-nc01/resources.yaml",
    bootstrapApplicationPath: "deploy/gitops/unidesk-host/platform-infra-gitea-nc01-application.yaml",
    releaseStatePath: "deploy/gitops-state/platform-infra/gitea-nc01.json",
    application: {
      name: "platform-infra-gitea-nc01",
      namespace: "argocd",
      project: "default",
      repoUrl: fileUrl(gitOpsBare),
      targetRevision: "unidesk-host-gitops",
      path: "deploy/gitops/platform-infra/gitea-nc01",
      destinationNamespace: "devops-infra",
      automated: true,
    },
    author: { name: "UniDesk Test", email: "unidesk-test@example.invalid" },
    cas: { maxAttempts: 3 },
  };
  return {
    root,
    sourceWork,
    gitOpsBare,
    publisherWorktree: join(root, "publisher-worktree"),
    commits: { first, second, diverged },
    delivery,
    desiredManifest: `---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: bridge
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: inbox
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bridge
`,
  };
}

function publish(fixture: ReturnType<typeof createFixture>, sourceCommit: string) {
  return publishPlatformInfraGiteaGitOps({
    delivery: fixture.delivery,
    desiredManifest: fixture.desiredManifest,
    sourceRoot: fixture.sourceWork,
    sourceCommit,
    worktree: fixture.publisherWorktree,
  });
}

function pushConcurrentFile(root: string, remote: string, branch: string) {
  const worktree = join(root, "concurrent-gitops");
  rmSync(worktree, { recursive: true, force: true });
  git(["clone", "--branch", branch, fileUrl(remote), worktree], root);
  identity(worktree);
  write(worktree, "deploy/gitops/unidesk-host/concurrent-todo.yaml", "concurrent todo\n");
  git(["add", "."], worktree);
  git(["commit", "-m", "concurrent todo update"], worktree);
  git(["push", "origin", branch], worktree);
}

function mutateGitOpsState(root: string, remote: string, branch: string, relativePath: string, mutate: (state: Record<string, unknown>) => Record<string, unknown>) {
  const worktree = join(root, "mutate-gitops-state");
  rmSync(worktree, { recursive: true, force: true });
  git(["clone", "--branch", branch, fileUrl(remote), worktree], root);
  identity(worktree);
  const path = join(worktree, relativePath);
  writeFileSync(path, `${JSON.stringify(mutate(JSON.parse(readFileSync(path, "utf8"))), null, 2)}\n`);
  git(["add", "--", relativePath], worktree);
  git(["commit", "-m", "inject conflicting materialization state"], worktree);
  git(["push", "origin", branch], worktree);
}

function checkout(worktree: string, commit: string) {
  git(["checkout", "--detach", commit], worktree);
}

function show(remote: string, branch: string, path: string): string {
  const result = git(["--git-dir", remote, "show", `${branch}:${path}`], dirname(remote));
  return result.stdout;
}

function write(root: string, relativePath: string, content: string) {
  const path = join(root, relativePath);
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, content);
  expect(readFileSync(path, "utf8")).toBe(content);
}

function identity(root: string) {
  git(["config", "user.name", "UniDesk Test"], root);
  git(["config", "user.email", "unidesk-test@example.invalid"], root);
}

function fileUrl(path: string): string {
  return `file://${path}`;
}

function git(args: string[], cwd: string) {
  const result = spawnSync("git", args, { cwd, encoding: "utf8" });
  if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
  return result;
}
