import { watch } from "node:fs";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { expect } from "e2e";
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NetService from "@t3tools/shared/Net";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as References from "effect/References";
import * as EnvironmentAuth from "../../apps/server/src/auth/EnvironmentAuth.ts";
import * as ServerConfig from "../../apps/server/src/config.ts";
import { resolveCliAuthConfig } from "../../apps/server/src/cli/config.ts";
import { mcpToolSession } from "../../apps/server/src/mcp/AcpMcpStdioBridge.ts";
import { readLiveServerRuntimeState } from "../../apps/server/src/serverRuntimeState.ts";
import { T3_HOME } from "../e2e.config.ts";
import { test, WEB, makeState, runPromise } from "./t3.ts";

const control = async (phase: number) => {
  await fetch("http://127.0.0.1:18942/control", {
    method: "POST", body: JSON.stringify({ phase }),
  });
};

const baselineEstablished = (threadId: string) => new Promise<void>((resolve, reject) => {
  const directory = join(T3_HOME, "userdata");
  const deadline = setTimeout(() => { watcher.close(); reject(new Error("The PR watch did not establish its pending-check baseline")); }, 145_000);
  const check = () => {
    const db = new DatabaseSync(join(directory, "statev2.sqlite"), { readOnly: true });
    try {
      const row = db.prepare("select payload_json from orchestration_v2_projection_threads where thread_id=?").get(threadId) as { payload_json: string } | undefined;
      const watchState = row && JSON.parse(row.payload_json).pullRequests?.[0]?.watch;
      if (watchState?.headSha && !watchState.awaitingBaseline) {
        clearTimeout(deadline); watcher.close(); resolve();
      }
    } finally { db.close(); }
  };
  const watcher = watch(directory, check);
  check();
});

const completedRuns = (threadId: string, count: number) => new Promise<void>((resolve, reject) => {
  const directory = join(T3_HOME, "userdata");
  const deadline = setTimeout(() => { watcher.close(); reject(new Error("The awakened agent did not finish its turn")); }, 30_000);
  const check = () => {
    const db = new DatabaseSync(join(directory, "statev2.sqlite"), { readOnly: true });
    try {
      const row = db.prepare("select count(distinct json_extract(payload_json, '$.id')) as count from orchestration_events where stream_id=? and event_type='run.updated' and json_extract(payload_json, '$.status')='completed'").get(threadId) as { count: number };
      if (row.count >= count) { clearTimeout(deadline); watcher.close(); resolve(); }
    } finally { db.close(); }
  };
  const watcher = watch(directory, check);
  check();
});

const withTools = async (use: (call: (name: string, args: Record<string, unknown>) => Promise<any>) => Promise<void>) => {
  await runPromise(Effect.gen(function* () {
    const config = yield* resolveCliAuthConfig({ baseDir: Option.some(T3_HOME) }, Option.none());
    return yield* Effect.gen(function* () {
      const runtime = yield* readLiveServerRuntimeState(config.serverRuntimeStatePath);
      if (Option.isNone(runtime)) throw new Error("Scenario server is not running");
      const auth = yield* EnvironmentAuth.EnvironmentAuth;
      const issued = yield* Effect.acquireRelease(
        auth.issueMcpClientSession({ label: "CI watch scenario", access: "full-access", peerLink: false, client: { deviceType: "bot" } }),
        (session) => auth.revokeSession(session.sessionId).pipe(Effect.ignore),
      );
      const session = yield* mcpToolSession({ endpoint: new URL("/mcp", runtime.value.origin).href, authorization: `Bearer ${issued.token}`, clientName: "watch-scenario" });
      yield* Effect.tryPromise(() => use(async (name, args) => {
        const result = await Effect.runPromise(session.call(name, args)) as any;
        if (result.isError) throw new Error(JSON.stringify(result));
        return result.structuredContent;
      }));
    }).pipe(Effect.scoped, Effect.provide(EnvironmentAuth.layerRuntime.pipe(
      Layer.provide(ServerConfig.layer(config)),
      Layer.provide(Layer.succeed(References.MinimumLogLevel, config.logLevel)),
    )));
  }).pipe(Effect.provide([NodeCrypto.layer, NetService.layer])));
};

test("PR watch wakes again when the remaining checks finish after an early failure", WEB,
  async ({ t3, app, screen, browser }) => {
    const capture = async (label: string) => {
      await browser.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
      for (let attempt = 0; attempt < 20; attempt++) {
        const before = await app.screenshot(`${label}-stability-${attempt}`);
        await new Promise((resolve) => setTimeout(resolve, 500));
        const after = await app.screenshot(label);
        if ((await readFile(before)).equals(await readFile(after))) return;
      }
      throw new Error(`The ${label} capture did not become visually stable`);
    };
    await control(0);
    const project = await t3.project("ci-watch-demo");
    await t3.thread({ project, title: "Watch CI until all checks finish", messages: ["Watch this pull request. Report failures, then keep waiting for the remaining checks."], model: "multi-turn" });
    const listing = await runPromise(makeState(T3_HOME).cli(["thread", "list", "--project", project, "--json"]));
    const threadId = JSON.parse(listing.stdout)[0].threadId;
    await t3.open();
    await screen.getByRole("button", "Watch CI until all checks finish, Done, ci-watch-demo").tap();
    await withTools(async (call) => {
      await call("watch_pull_request", { threadId, url: "https://github.com/scenario/demo/pull/42" });
      // Wait for the real reactor to commit its initial pending-check baseline.
      await baselineEstablished(threadId);
      await screen.getByText("Synthetic CI watch demonstration", { exact: false }).first().waitFor({ timeout: 145_000 });

      await control(1);
      await screen.getByText("#42: checks failed", { exact: true }).first().waitFor({ timeout: 145_000 });
      await completedRuns(threadId, 2);
      await screen.getByRole("button", "Stop generation").waitFor({ state: "hidden" });
      await screen.getByRole("button", "Open checks: 1 of 3 failing").tap();
      await new Promise((resolve) => setTimeout(resolve, 3_000));
      await browser.keyboard.press("Escape");
      await control(2);
      const finished = screen.getByText("#42: checks finished", { exact: true });
      try {
        await expect(finished).toBeVisible({ timeout: 145_000 });
      } finally {
        if (await finished.isVisible()) {
          await completedRuns(threadId, 3);
          await screen.getByRole("button", "Stop generation").waitFor({ state: "hidden" });

        }
        await screen.getByRole("button", "Open checks: 1 of 3 failing").tap();
        await capture("remaining-checks-finished");
        await browser.keyboard.press("Escape");
      }
    });
  },
);
