/// <reference lib="dom" />
// The DOM lib types the functions this scenario runs in the page.
import { expect } from "e2e";
import { readFile } from "node:fs/promises";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";

import { T3_HOME } from "../e2e.config.ts";
import { runPromise, test, WEB } from "./t3.ts";

const SettingsJson = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown));
const decodeSettings = Schema.decodeEffect(SettingsJson);
const encodeSettings = Schema.encodeEffect(SettingsJson);
interface CodexInstance {
  readonly config?: Readonly<Record<string, unknown>>;
}

interface ConfigHold {
  active: boolean;
  receipts: boolean;
  lastSave?: any;
  rawSend?: (data: string) => void;
  writerId: number;
  configTexts: string[];
  readonly replies: Array<() => void>;
  readonly held: Array<() => void>;
  readonly saveReplies: Set<string>;
}

declare global {
  interface Window {
    configHold?: ConfigHold;
  }
}

const settingsFile = Effect.gen(function* () {
  const fs = yield* FileSystem.FileSystem;
  const settingsPath = (yield* Path.Path).join(T3_HOME, "userdata/settings.json");
  const original = yield* fs.readFileString(settingsPath);
  const settings = yield* decodeSettings(original);
  const instances = settings.providerInstances as Record<string, CodexInstance>;
  const codex = instances.codex!;
  const customModels = [
    { slug: "model-a", name: "Original A" },
    { slug: "model-b", name: "Original B" },
  ];
  return {
    block: Effect.gen(function* () { yield* fs.rename(settingsPath, settingsPath + ".backup"); yield* fs.makeDirectory(settingsPath); }),
    unblock: Effect.gen(function* () { yield* fs.remove(settingsPath, { recursive: true }); yield* fs.rename(settingsPath + ".backup", settingsPath); }),
    seed: fs.writeFileString(
      settingsPath,
      yield* encodeSettings({
        ...settings,
        providerInstances: {
          ...instances,
          codex: { ...codex, config: { ...codex.config, binaryPath: "scenario-provider", customModels } },
        },
      }),
    ),
    savedNames: fs.readFileString(settingsPath).pipe(
      Effect.flatMap(decodeSettings),
      Effect.map((saved) => {
        const config = (saved.providerInstances as Record<string, CodexInstance>).codex?.config;
        const models = (config?.customModels ?? []) as ReadonlyArray<{ readonly name: string }>;
        return models.map((model) => model.name);
      }),
    ),
    restore: fs.writeFileString(settingsPath, original),
  };
});

test(
  "Two custom model edits saved before the first one's settings echo both persist",
  WEB,
  async ({ t3, app, browser, screen }) => {
    // Holds the server config stream while active, so a save's reply arrives before its settings
    // echo, and records settings save replies by request id.
    await browser.addInitScript(() => {
      const hold: ConfigHold = { active: false, receipts: false, writerId: 900000, configTexts: [], replies: [], held: [], saveReplies: new Set() };
      window.configHold = hold;
      const tags = new Map<string, string>();
      const send = WebSocket.prototype.send;
      WebSocket.prototype.send = function (data) {
        if (typeof data === "string") {
          for (const message of [JSON.parse(data)].flat()) {
            if (message?._tag === "Request") {
              tags.set(String(message.id), message.tag);
              if (message.tag === "server.updateSettings") {
                hold.lastSave = structuredClone(message);
                hold.rawSend = (data) => { const request = JSON.parse(data); tags.set(String(request.id), request.tag); send.call(this, data); };
              }
            }
          }
        }
        return send.call(this, data);
      };
      const addEventListener = WebSocket.prototype.addEventListener;
      WebSocket.prototype.addEventListener = function (
        this: WebSocket,
        type: string,
        listener: EventListenerOrEventListenerObject,
        options?: boolean | AddEventListenerOptions,
      ) {
        if (type !== "message" || typeof listener !== "function") {
          return addEventListener.call(this, type, listener, options);
        }
        const wrapped = (event: Event) => {
          const data = (event as MessageEvent).data;
          if (typeof data !== "string") return listener.call(this, event);
          const parsed = JSON.parse(data.replace(/\/[^"\\\s]*\/userdata\/settings\.json/g, "scenario-settings.json"));
          const messages = [parsed].flat();
          const isConfig = (message: { requestId?: unknown }) =>
            tags.get(String(message.requestId)) === "subscribeServerConfig";
          for (const message of messages) {
            if (
              message?._tag === "Exit" &&
              tags.get(String(message.requestId)) === "server.updateSettings"
            ) {
              hold.saveReplies.add(String(message.requestId));
            }
          }
          const deliver = (part: ReadonlyArray<unknown>) =>
            listener.call(
              this,
              new MessageEvent("message", {
                data: JSON.stringify(Array.isArray(parsed) ? part : part[0]),
              }),
            );
          const held = hold.active ? messages.filter(isConfig) : [];
          const replies = hold.receipts ? messages.filter((message) => message?._tag === "Exit" && tags.get(String(message.requestId)) === "server.updateSettings") : [];
          const passed = messages.filter((message) => !held.includes(message) && !replies.includes(message));
          if (replies.length > 0) hold.replies.push(() => deliver(replies));
          if (held.length > 0) { hold.configTexts.push(JSON.stringify(held)); hold.held.push(() => deliver(held)); }
          if (passed.length > 0) deliver(passed);
        };
        return addEventListener.call(this, type, wrapped, options);
      } as typeof WebSocket.prototype.addEventListener;
    });
    const hold = (active: boolean) =>
      browser.evaluate((active) => {
        const hold = window.configHold!;
        hold.active = active;
        if (!active) for (const deliver of hold.held.splice(0)) deliver();
        return null;
      }, active);
    const saveReplies = () => browser.evaluate(() => window.configHold!.saveReplies.size);
    const rename = async (slug: string, name: string) => {
      await screen.getByRole("button", `Edit ${slug}`, { exact: true }).tap();
      await screen.getByRole("textbox", "Display name", { exact: true }).fill(name);
      await screen.getByRole("button", "Save", { exact: true }).tap();
    };

    const pause = () => browser.evaluate(() => new Promise<null>((resolve) => setTimeout(() => resolve(null), 900)));
    const capture = async (label: string) => {
      await browser.evaluate(() => { (document.activeElement as HTMLElement | null)?.blur(); return null; });
      await pause();
      for (let attempt = 0; attempt < 5; attempt++) {
        const first = await app.screenshot(`${label}-stability`);
        await browser.evaluate(() => new Promise<null>((resolve) => setTimeout(() => resolve(null), 500)));
        const second = await app.screenshot(label);
        if ((await readFile(first)).equals(await readFile(second))) return;
      }
      throw new Error(`Unstable capture: ${label}`);
    };
    const receiptHold = (active: boolean) => browser.evaluate((active) => {
      const h = window.configHold!; h.receipts = active;
      if (!active) for (const deliver of h.replies.splice(0)) deliver();
      return null;
    }, active);
    const draft = () => browser.evaluate(() => (document.getElementById(Array.from(document.querySelectorAll("label")).find((label) => label.textContent?.trim() === "Display name")?.htmlFor ?? "") as HTMLInputElement | null)?.value ?? null);
    const openEdit = async (slug: string) => {
      await screen.getByRole("button", `Edit ${slug}`, { exact: true }).tap();
    };
    const reopen = async () => {
      await screen.getByRole("button", "General", { exact: true }).tap();
      await screen.getByRole("button", "Providers", { exact: true }).tap();
      await screen.getByRole("button", "Edit model-a", { exact: true }).waitFor();
      await screen.getByRole("button", "Edit model-b", { exact: true }).scrollIntoView();
    };
    const otherWriter = async (names: string[]) => {
      const before = await saveReplies();
      await browser.evaluate((names) => {
        const h = window.configHold!;
        const request = structuredClone(h.lastSave);
        request.id = String(++h.writerId);
        request.payload.providerInstanceMutation.instance.config.customModels = names.map((name, i) => ({ slug: i === 0 ? "model-a" : "model-b", name }));
        h.rawSend!(JSON.stringify(request));
        return null;
      }, names);
      await expect.poll(saveReplies).toBe(before + 1);
      await expect.poll(() => browser.evaluate((name) => window.configHold!.configTexts.some((text) => text.includes(name)), names[0])).toBe(true);
    };
    const coalesce = () => browser.evaluate(() => {
      const h = window.configHold!; h.active = false;
      const last = h.held.at(-1); h.held.length = 0; last?.(); return null;
    });
    const settings = await runPromise(settingsFile);
    const observations: Record<string, unknown> = {};
    try {
      await t3.project("Model save demonstration");
      await runPromise(settings.seed);
      await t3.open("/settings/providers?instanceId=codex");
      await screen.getByRole("button", "Edit model-b", { exact: true }).scrollIntoView();
      await capture("initial-two-models");
      const before = await saveReplies();
      await hold(true);
      await rename("model-a", "Entered A");
      await expect.poll(saveReplies).toBe(before + 1);
      await rename("model-b", "Entered B");
      await expect.poll(saveReplies).toBe(before + 2);
      await hold(false);
      await app.open("/settings/providers?instanceId=codex");
      await screen.getByRole("button", "Edit model-b", { exact: true }).scrollIntoView();
      await capture("successive-saves-after-reload");
      observations.successive = await runPromise(settings.savedNames);

      // A real filesystem write failure, rather than a synthetic RPC success/failure.
      await openEdit("model-a");
      await screen.getByRole("textbox", "Display name", { exact: true }).fill("Unsaved draft A");
      await runPromise(settings.block);
      const failedBefore = await saveReplies();
      await screen.getByRole("button", "Save", { exact: true }).tap();
      await expect.poll(saveReplies).toBe(failedBefore + 1);
      await runPromise(settings.unblock);
      await capture("failed-save-keeps-draft");
      await screen.getByRole("button", "Dismiss notification", { exact: true }).tap();
      observations.failedDraft = await draft();
      await reopen();

      // Save is accepted, but its receipt reaches the editor only after a newer draft exists.
      await hold(true);
      await receiptHold(true);
      const lateBefore = await saveReplies();
      await rename("model-a", "Accepted A");
      await expect.poll(saveReplies).toBe(lateBefore + 1);
      await capture("pending-save-before-newer-draft");
      if (await draft() === null) await openEdit("model-a");
      await screen.getByRole("textbox", "Display name", { exact: true }).fill("Newer draft A");
      await capture("newer-draft-before-late-receipt");
      await receiptHold(false);
      await capture("newer-draft-after-late-receipt");
      observations.newerDraft = await draft();
      await hold(false);
      await reopen();

      // Switch editors while the first model's receipt is outstanding.
      await hold(true); await receiptHold(true);
      const switchedBefore = await saveReplies();
      await rename("model-a", "Accepted A again");
      await expect.poll(saveReplies).toBe(switchedBefore + 1);
      await openEdit("model-b");
      await screen.getByRole("textbox", "Display name", { exact: true }).fill("Newer draft B");
      await receiptHold(false);
      await capture("newer-editor-after-late-receipt");
      observations.newerEditor = await draft();
      await hold(false); await reopen();

      // Another writer replaces A after our save receipt but before its held echo.
      await hold(true);
      const writerBefore = await saveReplies();
      await rename("model-a", "Local A");
      await expect.poll(saveReplies).toBe(writerBefore + 1);
      await otherWriter(["Other writer A", "Other writer B"]);
      // Coalesce the stream: deliver only the newest configuration message.
      await coalesce();
      await pause();
      const finalBefore = await saveReplies();
      await rename("model-b", "Final B");
      await expect.poll(saveReplies).toBe(finalBefore + 1);
      await reopen();
      await capture("other-writer-and-coalesced-echo-after-reload");
      observations.otherWriter = await runPromise(settings.savedNames);

      // A raced in-flight save must also retire after its receipt finally arrives.
      await hold(true); await receiptHold(true);
      const inFlightBefore = await saveReplies();
      await rename("model-a", "In-flight A");
      await expect.poll(saveReplies).toBe(inFlightBefore + 1);
      await otherWriter(["Remote winner A", "Remote winner B"]);
      await coalesce(); await pause();
      await receiptHold(false); await pause();
      // Close the prior editor if this side kept it open.
      if (await draft() !== null) await screen.getByRole("button", "Cancel", { exact: true }).tap();
      const endBefore = await saveReplies();
      await rename("model-b", "Final in-flight B");
      await expect.poll(saveReplies).toBe(endBefore + 1);
      await reopen();
      await capture("other-writer-before-late-receipt-after-reload");
      observations.inFlightWriter = await runPromise(settings.savedNames);

      // Repeated pending snapshots S1, S2, S1 with only the last echo delivered.
      await hold(true);
      for (const [slug, name] of [["model-a", "Repeated A"], ["model-b", "Transient B"], ["model-b", "Final in-flight B"]]) {
        const before = await saveReplies();
        await rename(slug!, name!);
        await expect.poll(saveReplies).toBe(before + 1);
      }
      await coalesce(); await pause();
      await hold(true);
      await otherWriter(["Coalesced writer A", "Coalesced writer B"]);
      await coalesce(); await pause();
      const repeatedBefore = await saveReplies();
      await rename("model-b", "Final coalesced B");
      await expect.poll(saveReplies).toBe(repeatedBefore + 1);
      await reopen();
      await capture("repeated-coalesced-echo-after-reload");
      observations.repeated = await runPromise(settings.savedNames);

      // Record all visible cases on both commits before asserting their outcomes.
      expect(observations).toEqual({
        successive: ["Entered A", "Entered B"],
        failedDraft: "Unsaved draft A",
        newerDraft: "Newer draft A",
        newerEditor: "Newer draft B",
        otherWriter: ["Other writer A", "Final B"],
        inFlightWriter: ["Remote winner A", "Final in-flight B"],
        repeated: ["Coalesced writer A", "Final coalesced B"],
      });
    } finally {
      await runPromise(settings.restore);
    }
  },
);
