// Evidence driver for PR #23586 (late session/load adoption). Not a regression test:
// every row records what it observes and never asserts on behavior, so the same
// bytes produce the before and after transcripts.
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFS from "node:fs";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { ProviderDriverKind, ProviderInstanceId, ProviderSessionId, ThreadId } from "@t3tools/contracts";
import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
import { AcpProviderCapabilitiesV2, makeAcpAdapterV2 } from "@t3tools/provider-acp/server/adapter";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as Stream from "effect/Stream";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import { describe } from "vite-plus/test";

const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
const mockAgentPath = NodePath.join(__dirname, "../../../scripts/acp-mock-agent.ts");
const proxyPath = "/tmp/ev-23586/driver/load-error-proxy.mjs";
const tree = process.cwd().endsWith("-base") ? "before" : "after";
const outFile = `/tmp/ev-23586/rows-${tree}.txt`;
const t0 = Date.now();

const LOAD_DELAY_MS = 1500;
const IDLE_GAP = "100 millis";
const SETTLE_MS = LOAD_DELAY_MS + 1500;

const emit = (line: string) => {
  const full = `${tree} ${line}`;
  NodeFS.appendFileSync(outFile, full + "\n");
  process.stdout.write(full + "\n");
};

const fmtOptions = (options: ReadonlyArray<any> | undefined) =>
  JSON.stringify((options ?? []).map((o) => [o.id, o.category ?? null, o.currentValue]));

const shortCause = (cause: Cause.Cause<unknown> | undefined) => {
  if (cause === undefined) return "?";
  if (Cause.hasInterruptsOnly(cause)) return "interrupted";
  const err = Cause.squash(cause) as any;
  return String(err?.detail ?? err?.message ?? err?._tag ?? err).slice(0, 80);
};

interface Row {
  readonly id: string;
  readonly label: string;
  readonly env?: Record<string, string>;
  readonly spawn?: { readonly command: string; readonly args: ReadonlyArray<string> };
  readonly runtime?: Partial<AcpSessionRuntime.AcpSessionRuntimeOptions>;
  readonly closeAfterMs?: number;
  readonly action?: (
    runtime: AcpSessionRuntime.AcpSessionRuntime["Service"],
  ) => Effect.Effect<string, unknown>;
  readonly settleMs?: number;
  readonly promptAfter?: boolean;
}

const runRow = (row: Row) =>
  Effect.gen(function* () {
    const rowStart = Date.now();
    const loadLog: Array<string> = [];
    const eventTags: Array<string> = [];
    let startMs = -1;
    let meta = "?";
    let action = "-";
    let post = "-";
    let snapshot: { options: string; mode: string } = { options: "?", mode: "?" };

    const body = Effect.gen(function* () {
      const runtime = yield* AcpSessionRuntime.make({
        spawn: {
          ...(row.spawn ?? { command: "node", args: [mockAgentPath] }),
          env: {
            T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY: "1",
            T3_ACP_LOAD_SESSION_DELAY_MS: String(LOAD_DELAY_MS),
            ...row.env,
          },
        },
        cwd: process.cwd(),
        clientInfo: { name: "t3-evidence", version: "0.0.0" },
        authMethodId: "test",
        resumeSessionId: "mock-session-1",
        sessionLoadReplayIdleGap: IDLE_GAP,
        requestLogger: (event) =>
          event.method === "session/load"
            ? Effect.sync(() => {
                const at = Date.now() - rowStart;
                if (event.status === "succeeded") {
                  const result = event.result as any;
                  const kind =
                    result?._meta?.t3SessionLoadReady === "replay_idle"
                      ? "idle"
                      : result?.configOptions
                        ? `rpc:${fmtOptions(result.configOptions)}`
                        : "rpc";
                  loadLog.push(`succeeded(${kind})@${at}`);
                } else if (event.status === "failed") {
                  loadLog.push(`failed(${shortCause(event.cause)})@${at}`);
                } else {
                  loadLog.push(`started@${at}`);
                }
              })
            : Effect.void,
        ...row.runtime,
      });
      yield* runtime.getEvents().pipe(
        Stream.runForEach((event) => Effect.sync(() => eventTags.push((event as any)._tag))),
        Effect.forkScoped,
      );
      const started = yield* runtime.start();
      startMs = Date.now() - rowStart;
      meta = JSON.stringify((started.sessionSetupResult as any)._meta ?? null);
      if (row.action) {
        action = yield* row.action(runtime).pipe(
          Effect.catchCause((cause) => Effect.succeed(`error(${shortCause(cause)})`)),
        );
      }
      if (row.closeAfterMs !== undefined) {
        yield* Effect.sleep(`${row.closeAfterMs} millis`);
        return runtime;
      }
      yield* Effect.sleep(`${row.settleMs ?? SETTLE_MS} millis`);
      if (row.promptAfter) {
        post = yield* runtime
          .prompt({ prompt: [{ type: "text", text: "after late load" }] })
          .pipe(
            Effect.timeout("10 seconds"),
            Effect.map((r: any) => `prompt:${r?.stopReason ?? "ok"}`),
            Effect.catchCause((cause) => Effect.succeed(`prompt-error(${shortCause(cause)})`)),
          );
      }
      snapshot = {
        options: fmtOptions(yield* runtime.getConfigOptions),
        mode: String((yield* runtime.getModeState)?.currentModeId),
      };
      return runtime;
    }).pipe(Effect.scoped);

    const exit = yield* Effect.exit(body);
    if (row.closeAfterMs !== undefined && Exit.isSuccess(exit)) {
      // Scope already closed; wait past the agent's delayed reply.
      yield* Effect.sleep(`${SETTLE_MS} millis`);
      snapshot = {
        options: fmtOptions(yield* exit.value.getConfigOptions),
        mode: String((yield* exit.value.getModeState)?.currentModeId),
      };
    }
    const crash = Exit.isSuccess(exit) ? "none" : `exit-failure(${shortCause(exit.cause)})`;
    const adopted = eventTags.filter((t) => t === "SessionSetupAdopted").length;
    const cfgUpdates = eventTags.filter((t) => t === "ConfigOptionsUpdated").length;
    const terminated = eventTags.filter((t) => t === "ConnectionTerminated").length;
    emit(
      `[${row.id} ${row.label}] t=${Date.now() - t0} start()=${startMs} meta=${meta} options=${snapshot.options} mode=${snapshot.mode} events=SessionSetupAdopted:${adopted},ConfigOptionsUpdated:${cfgUpdates},ConnectionTerminated:${terminated} action=${action} post=${post} log=${loadLog.join(",")} crash=${crash}`,
    );
  });

const rows: ReadonlyArray<Row> = [
  { id: "R1", label: "final response before idle (control)", env: { T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY: "0", T3_ACP_EMIT_LOAD_REPLAY: "1" } },
  { id: "R2", label: "late response after idle (control)" },
  {
    id: "R4b-mode",
    label: "setMode('code') before late response",
    action: (runtime) => runtime.setMode("code").pipe(Effect.as("setMode:ok")),
  },
  {
    id: "R4b-model",
    label: "setSessionModel('gpt-5.4') (session/set_model) before late response",
    action: (runtime) =>
      runtime.setSessionModel("gpt-5.4").pipe(Effect.as("setSessionModel:ok")),
  },
  {
    id: "R5",
    label: "agent config/mode notification before late response",
    action: (runtime) =>
      runtime.request("_test/startup-metadata", {}).pipe(Effect.as("startup-metadata:ok")),
  },
  {
    id: "R6",
    label: "superseded by loadSession",
    settleMs: SETTLE_MS + LOAD_DELAY_MS,
    action: (runtime) =>
      runtime.loadSession("mock-session-1").pipe(
        Effect.map((r) => `loadSession:${JSON.stringify((r.sessionSetupResult as any)._meta ?? null)}`),
      ),
  },
  {
    id: "R7",
    label: "superseded by resumeSession",
    action: (runtime) =>
      runtime.resumeSession("mock-session-1").pipe(Effect.map((r) => `resumeSession:${r.sessionId}`)),
  },
  {
    id: "R8",
    label: "superseded by forkSession",
    env: { T3_ACP_SESSION_LIFECYCLE: "1" },
    action: (runtime) =>
      runtime.forkSession("mock-session-1").pipe(Effect.map((r) => `forkSession:${r.sessionId}`)),
  },
  { id: "R9", label: "runtime scope closed before late response", closeAfterMs: 200 },
  {
    id: "R10",
    label: "late JSON-RPC error response (stdio proxy)",
    spawn: { command: "node", args: [proxyPath, mockAgentPath] },
    promptAfter: true,
  },
  {
    id: "R11",
    label: "late timeout (sessionLoadTimeout 1s < delay 3s)",
    env: { T3_ACP_LOAD_SESSION_DELAY_MS: "3000" },
    runtime: { sessionLoadTimeout: "1 second" },
    settleMs: 3500,
    promptAfter: true,
  },
];

const layerTest = Layer.mergeAll(
  NodeServices.layer,
  IdAllocator.layer,
  McpProviderSessions.layer,
  TestProviderHost.layer().pipe(Layer.provide(NodeServices.layer)),
);

describe("late session/load evidence", () => {
  NodeFS.rmSync(outFile, { force: true });
  for (const row of rows) {
    it.live(`${row.id} ${row.label}`, () => runRow(row).pipe(Effect.provide(NodeServices.layer)), 60_000);
  }

  it.live(
    "R12 adapter: supportsModelSwitchInSession + provider_session.updated",
    () =>
      Effect.gen(function* () {
        const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
        const selfInvocation = yield* resolveSelfInvocation();
        const instanceId = ProviderInstanceId.make("acp-evidence-late-load");
        const updates: Array<string> = [];
        const exit = yield* Effect.exit(
          Effect.gen(function* () {
            const adapter = yield* makeAcpAdapterV2({
              instanceId,
              flavor: {
                driver: ProviderDriverKind.make("acp-evidence"),
                capabilities: AcpProviderCapabilitiesV2,
                makeRuntime: (runtimeInput) =>
                  Effect.gen(function* () {
                    const context = yield* Layer.build(
                      AcpSessionRuntime.layer({
                        ...runtimeInput,
                        sessionLoadReplayIdleGap: IDLE_GAP,
                        spawn: {
                          command: process.execPath,
                          args: [mockAgentPath],
                          cwd: runtimeInput.cwd,
                          env: {
                            T3_ACP_SESSION_LIFECYCLE: "1",
                            T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY: "1",
                            T3_ACP_LOAD_SESSION_DELAY_MS: String(LOAD_DELAY_MS),
                          },
                        },
                        authMethodId: "test",
                      }).pipe(
                        Layer.provide(
                          Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
                        ),
                      ),
                    );
                    return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(
                      Effect.provide(context),
                    );
                  }),
              },
              selfInvocation,
            } as any);
            const openedAt = Date.now();
            const runtime = yield* adapter.openSession({
              threadId: ThreadId.make("thread-acp-evidence-late-load"),
              providerSessionId: ProviderSessionId.make("provider-session-acp-evidence-late-load"),
              modelSelection: { instanceId, model: "default" },
              runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
                runtimeMode: "full-access",
                interactionMode: "default",
                cwd: process.cwd(),
              }),
              initialNativeThreadId: "mock-session-1",
            } as any);
            const atOpen =
              runtime.providerSession.capabilities.sessions.supportsModelSwitchInSession;
            const openMs = Date.now() - openedAt;
            yield* runtime.events.pipe(
              Stream.runForEach((event: any) =>
                Effect.sync(() => {
                  if (event.type === "provider_session.updated") {
                    updates.push(
                      `${event.providerSession.capabilities.sessions.supportsModelSwitchInSession}@${Date.now() - openedAt}`,
                    );
                  }
                }),
              ),
              Effect.ignore,
              Effect.forkScoped,
            );
            yield* Effect.sleep(`${SETTLE_MS} millis`);
            const atEnd =
              runtime.providerSession.capabilities.sessions.supportsModelSwitchInSession;
            return { atOpen, atEnd, openMs };
          }).pipe(Effect.scoped),
        );
        const crash = Exit.isSuccess(exit) ? "none" : `exit-failure(${shortCause(exit.cause)})`;
        const v = Exit.isSuccess(exit) ? exit.value : undefined;
        emit(
          `[R12 adapter supportsModelSwitchInSession] t=${Date.now() - t0} openSession()=${v?.openMs} supportsModelSwitchInSession: atOpen=${v?.atOpen} afterLateResponse=${v?.atEnd} events=provider_session.updated:[${updates.join(",")}] crash=${crash}`,
        );
      }).pipe(Effect.provide(layerTest)),
    60_000,
  );
});
