import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import ts from "typescript";
import { act, createElement } from "react";
import { createRoot } from "react-dom/client";
import { RegistryContext, useAtomValue } from "@effect/atom-react";
import { Atom, AtomRegistry, AsyncResult } from "effect/reactivity";
import * as Effect from "effect/Effect";
import { afterEach, expect, it } from "vite-plus/test";
import { usePullToRefresh } from "/home/ubuntu/wt/pull-request-05/apps/mobile/src/state/use-pull-to-refresh.ts";

const cwd = "/home/ubuntu/wt/pull-request-05";
const baseline = process.env.PROOF_BASE === "1";
const source = (path: string) => baseline
  ? execFileSync("git", ["show", "origin/main:" + path], { cwd, encoding: "utf8" })
  : readFileSync(cwd + "/" + path, "utf8");
function extract(path: string, predicate: (node: ts.Node) => boolean) {
  const text = source(path);
  const file = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
  let found: ts.Node | undefined;
  function visit(node: ts.Node) { if (!found && predicate(node)) found = node; ts.forEachChild(node, visit); }
  visit(file);
  if (!found) throw Error("Production closure not found in " + path);
  return found.getText(file);
}
const refreshDeclaration = extract("apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx",
  n => ts.isVariableDeclaration(n) && n.name.getText() === "refresh") ;
const refreshExpression = refreshDeclaration.slice(refreshDeclaration.indexOf("=") + 1);
const compiledRefresh = ts.transpileModule("(" + refreshExpression + ")", { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText.replace(/^"use strict";\s*/, "");
const targetDeclaration = extract("apps/mobile/src/features/pull-requests/usePullRequestDiff.ts",
  n => ts.isFunctionDeclaration(n) && n.name?.text === "pullRequestTarget").replace(/^export /, "");
const compiledTarget = ts.transpileModule(targetDeclaration, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText;
const requestTarget = new Function("EnvironmentId", "ProjectId", compiledTarget + "; return pullRequestTarget;")(
  { make: (x: string) => x }, { make: (x: string) => x });
const params = { environmentId: "selected-environment", projectId: "selected-project", repository: "owner/repo", number: 17, host: "github.enterprise.test" };
function deferred<T = void>() {
  let resolve!: (value: T) => void, reject!: (error: unknown) => void;
  const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; });
  return { promise, resolve, reject };
}
const cleanups: (() => void)[] = [];
afterEach(async () => { await act(async () => { for (const cleanup of cleanups.splice(0)) cleanup(); }); });
function fixture(guidesEnabled = true) {
  (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
  const registry = AtomRegistry.make();
  const root = createRoot(document.createElement("div"));
  let held = "pending", upstream = "pending", reads = 0, freshReadGate: ReturnType<typeof deferred> | null = null;
  let readStarted = deferred(), readFinished = deferred();
  const events: string[] = [], inputs: unknown[] = [];
  const atom = Atom.make(Effect.gen(function*() {
    reads++; events.push("detail"); readStarted.resolve();
    if (freshReadGate) yield* Effect.promise(() => freshReadGate!.promise);
    const result = held;
    readFinished.resolve();
    return result;
  }));
  let api!: ReturnType<typeof usePullToRefresh>, visible = "", refresh!: () => void, invalidations = 0;
  const invalidateGate = deferred();
  let reject = false;
  const dependencies = {
    composerPullRequests: { invalidate: { run: async (_registry: unknown, input: unknown) => {
      invalidations++; inputs.push(input); events.push("invalidate:start");
      await invalidateGate.promise;
      if (reject) { events.push("invalidate:rejected"); throw Error("offline"); }
      held = upstream; events.push("invalidate:done"); return { _tag: "Success" };
    } } },
    requestTarget, params, registry, guidesEnabled,
    refreshGuide: () => events.push("guide"),
    stack: { refresh: () => events.push("stack") },
    refreshActivity: () => events.push("activity"),
    filesContent: { refresh: () => events.push("files") },
  };
  function Probe({ selected = atom }: { selected?: typeof atom | null }) {
    api = usePullToRefresh(selected);
    const result = useAtomValue(atom);
    if (AsyncResult.isSuccess(result)) visible = result.value;
    const keys = Object.keys(dependencies);
    refresh = new Function("pullRefresh", ...keys, "return " + compiledRefresh)(api, ...Object.values(dependencies));
    return null;
  }
  const render = (selected: typeof atom | null = atom) => act(async () => root.render(createElement(RegistryContext.Provider,
    { value: registry }, createElement(Probe, { selected }))));
  let unmounted = false;
  cleanups.push(() => { if (!unmounted) root.unmount(); registry.dispose(); });
  return {
    render, events, inputs, invalidateGate, registry, atom,
    unmount() { root.unmount(); unmounted = true; },
    get api() { return api; }, get visible() { return visible; }, get reads() { return reads; },
    get invalidations() { return invalidations; },
    updateUpstream() { upstream = "success"; },
    rejectInvalidation() { reject = true; },
    blockRead() { freshReadGate = deferred(); readStarted = deferred(); readFinished = deferred(); return { gate: freshReadGate, started: readStarted, finished: readFinished }; },
    pull() { refresh(); },
  };
}

it("actual route closure and hook replace held pending checks after invalidation; spinner owns both phases and deduplicates pulls", async () => {
  const f = fixture(); await f.render(); expect(f.visible).toBe("pending"); f.events.length = 0;
  f.updateUpstream();
  const read = f.blockRead();
  await act(async () => { f.pull(); f.pull(); });
  expect(f.api.refreshing).toBe(true);
  expect(f.events).toEqual(["invalidate:start"]);
  expect(f.invalidations).toBe(1);
  expect(f.inputs).toEqual([{environmentId: params.environmentId, input: {reference: {projectId: params.projectId, repository: params.repository, number: params.number, host: params.host}}}]);
  await act(async () => { f.invalidateGate.resolve(); await read.started.promise; });
  expect(f.events).toEqual(["invalidate:start", "invalidate:done", "guide", "stack", "activity", "files", "detail"]);
  expect(f.api.refreshing).toBe(true);
  await act(async () => { read.gate.resolve(); await read.finished.promise; });
  expect(f.visible).toBe("success");
  expect(f.api.refreshing).toBe(false);
  console.log("PASS pending -> success without TTL advance; exact environment/reference; dedup; spinner spans invalidation+detail", f.events);
});

it.each([true, false])("rejected invalidation still refreshes all sections, guide enabled=%s", async (guides) => {
  const f = fixture(guides); await f.render(); f.events.length = 0; f.rejectInvalidation();
  await act(async () => { f.pull(); });
  await act(async () => { f.invalidateGate.resolve(); });
  expect(f.events).toEqual(["invalidate:start", "invalidate:rejected", ...(guides ? ["guide"] : []), "stack", "activity", "files", "detail"]);
  expect(f.api.refreshing).toBe(false);
  console.log("PASS rejected invalidation fallback; guide conditional=" + guides, f.events);
});

it("changing selection while invalidation is pending prevents obsolete reads", async () => {
  const f = fixture(); await f.render(); f.events.length = 0;
  await act(async () => f.pull());
  await f.render(null); expect(f.api.refreshing).toBe(false);
  await act(async () => f.invalidateGate.resolve());
  expect(f.events).toEqual(["invalidate:start", "invalidate:done"]);
  console.log("PASS selection change prevents obsolete dependent/detail reads", f.events);
});

it("the unchanged zero-argument hook caller can still refresh", async () => {
  const f = fixture(); await f.render(); f.events.length = 0;
  await act(async () => f.api.onRefresh());
  expect(f.events).toEqual(["detail"]); expect(f.api.refreshing).toBe(false); expect(f.invalidations).toBe(0);
  console.log("PASS zero-argument refresh; no invalidation", f.events);
});

it("unmount during invalidation prevents late dependent and detail reads", async () => {
  const f = fixture(); await f.render(); f.events.length = 0;
  await act(async () => f.pull());
  await act(async () => f.unmount());
  await act(async () => f.invalidateGate.resolve());
  expect(f.events).toEqual(["invalidate:start", "invalidate:done"]);
});

it("old invalidation cannot clear a newer atom's spinner", async () => {
  const f = fixture(); await f.render(); f.events.length = 0;
  await act(async () => f.pull());
  const gate = deferred();
  const second = Atom.make(Effect.succeed("second"));
  await f.render(second);
  await act(async () => f.api.onRefresh(() => gate.promise));
  expect(f.api.refreshing).toBe(true);
  await act(async () => f.invalidateGate.resolve());
  expect(f.api.refreshing).toBe(true);
  expect(f.events).toEqual(["invalidate:start", "invalidate:done"]);
  await act(async () => gate.resolve());
  expect(f.api.refreshing).toBe(false);
});
