// @effect-diagnostics globalConsoleInEffect:off - console.log is required for outbound runtime evidence.
import type { SDKUserMessage } from "@cursor/sdk";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import {
  ChatAttachmentId,
  ChatImageAttachment,
  MessageId,
  NodeId,
  ProjectId,
  ProviderInstanceId,
  ProviderSessionId,
  RunAttemptId,
  RunId,
  ThreadId,
  type ChatAttachment,
} from "@t3tools/contracts";
import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
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 ProviderHost from "@t3tools/provider-core/server/ProviderHost";
import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeInstructions";
import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Stream from "effect/Stream";

import { makeCursorAdapterV2 } from "./adapter.ts";
import * as CursorAgentSdk from "./CursorAgentSdk.ts";

type Sent = {
  text: string;
  images: NonNullable<SDKUserMessage["images"]>;
  agent: string;
  summarize: boolean;
  accepted: boolean;
};

const carry = (previous: string, next: string) =>
  `Your previous turn was interrupted and is missing from your history. Some of its work may already be done. Its request was:\n\n${previous}\n\nThe new message, which may continue or change that request:\n\n${next}`;
const outbound = (text: string) =>
  `${text}\n\n${buildRuntimeInstructions({ harness: "Cursor", model: "auto" })}`;

const setup = (row: string) =>
  Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem;
    const workspace = yield* fs.makeTempDirectoryScoped({ prefix: `cursor-evidence-${row}-` });
    const services = yield* Layer.build(TestProviderHost.layer({ cwd: workspace }));
    const host = Context.get(services, ProviderHost.ProviderHost);
    const sent: Array<Sent> = [];
    const runEnds: Array<Deferred.Deferred<"finished" | "cancelled">> = [];
    let rejectNext = false;
    let created = 0;
    const instanceId = ProviderInstanceId.make("cursor");
    const threadId = ThreadId.make(`evidence-${row}`);
    const modelSelection = { instanceId, model: "auto" };
    const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
      runtimeMode: "full-access",
      interactionMode: "default",
      cwd: workspace,
    });
    const adapter = yield* makeCursorAdapterV2({
      instanceId,
      environment: { HOME: workspace },
    }).pipe(
      Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, {
        assertComplete: Effect.void,
        open: (openInput) => {
          const agentId = openInput.agentId ?? `native-agent-${++created}`;
          return Effect.succeed({
            agentId,
            listMessages: Effect.succeed([]),
            close: Effect.void,
            send: (input) =>
              Effect.gen(function* () {
                const record: Sent = {
                  text: typeof input.message === "string" ? input.message : input.message.text,
                  images: typeof input.message === "string" ? [] : (input.message.images ?? []),
                  agent: agentId,
                  summarize: input.summarize ?? false,
                  accepted: !rejectNext,
                };
                sent.push(record);
                console.log(`ROW ${row} send ${sent.length}: ${record.text}`);
                console.log(
                  `ROW ${row} send ${sent.length} payload: ${JSON.stringify({
                    agent: record.agent,
                    summarize: record.summarize,
                    accepted: record.accepted,
                    messageKind: typeof input.message === "string" ? "string" : "native-images",
                    images: record.images,
                  })}`,
                );
                if (rejectNext) {
                  rejectNext = false;
                  return yield* new CursorAgentSdk.CursorAgentSdkRunnerError({
                    method: "send",
                    cause: new Error("Synthetic rejection before acceptance"),
                  });
                }
                const runId = `evidence-run-${sent.length}`;
                const ended = yield* Deferred.make<"finished" | "cancelled">();
                runEnds.push(ended);
                return {
                  agentId,
                  runId,
                  wait: Deferred.await(ended).pipe(
                    Effect.map((status) => ({
                      id: runId,
                      requestId: `request-${runId}`,
                      status,
                      model: { id: "auto" },
                      durationMs: 1,
                    })),
                  ),
                  cancel: Deferred.succeed(ended, "cancelled"),
                };
              }),
          });
        },
      }),
      Effect.provide(services),
    );
    const runtime = yield* adapter.openSession({
      threadId,
      providerSessionId: ProviderSessionId.make(`evidence-session-${row}`),
      modelSelection,
      runtimePolicy,
    });
    const newThread = () => runtime.ensureThread({ threadId, modelSelection, runtimePolicy });
    const providerThread = yield* newThread();
    const now = yield* DateTime.now;
    const appThread = {
      id: threadId,
      projectId: ProjectId.make(`evidence-project-${row}`),
      createdBy: "user" as const,
      creationSource: "web" as const,
      title: `Synthetic evidence ${row}`,
      providerInstanceId: instanceId,
      modelSelection,
      runtimeMode: "full-access" as const,
      interactionMode: "default" as const,
      branch: null,
      worktreePath: null,
      activeProviderThreadId: providerThread.id,
      lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
      forkedFrom: null,
      createdAt: now,
      updatedAt: now,
      archivedAt: null,
      settledOverride: null,
      settledAt: null,
      lastVisitedAt: null,
      deletedAt: null,
    };
    let ordinal = 0;
    const turn = (
      text: string,
      attachments: ReadonlyArray<ChatAttachment> = [],
      target = providerThread,
    ) => {
      const n = ++ordinal;
      return {
        threadId,
        providerThread: target,
        modelSelection,
        runtimePolicy,
        runId: RunId.make(`evidence-${row}-run-${n}`),
        runOrdinal: n,
        providerTurnOrdinal: n,
        attemptId: RunAttemptId.make(`evidence-${row}-attempt-${n}`),
        rootNodeId: NodeId.make(`evidence-${row}-root-${n}`),
        appThread: { ...appThread, activeProviderThreadId: target.id },
        message: {
          messageId: MessageId.make(`evidence-${row}-message-${n}`),
          createdBy: "user" as const,
          creationSource: "web" as const,
          text,
          attachments,
        },
      };
    };
    const interrupt = () =>
      Effect.gen(function* () {
        const started = Option.getOrThrow(
          yield* runtime.events.pipe(
            Stream.filter((event) => event.type === "provider_turn.updated"),
            Stream.runHead,
          ),
        );
        assert(started.type === "provider_turn.updated");
        yield* runtime.interruptTurn({ providerThread, providerTurnId: started.providerTurn.id });
        yield* runtime.events.pipe(
          Stream.filter(
            (event) => event.type === "turn.terminal" && event.status === "interrupted",
          ),
          Stream.runHead,
        );
      });
    const finish = () =>
      Effect.gen(function* () {
        yield* Deferred.succeed(runEnds.at(-1)!, "finished");
        yield* runtime.events.pipe(
          Stream.filter((event) => event.type === "turn.terminal" && event.status === "completed"),
          Stream.runHead,
        );
      });
    return {
      sent,
      runtime,
      turn,
      interrupt,
      finish,
      newThread,
      fs,
      host,
      rejectNextSend: () => {
        rejectNext = true;
      },
    };
  });

const provide = <A, E>(
  program: Effect.Effect<
    A,
    E,
    | FileSystem.FileSystem
    | import("effect/Path").Path
    | IdAllocator.IdAllocatorV2
    | McpProviderSessions.McpProviderSessions
    | import("effect/Scope").Scope
  >,
) =>
  program.pipe(
    Effect.scoped,
    Effect.provide(
      Layer.mergeAll(NodeServices.layer, IdAllocator.layer, McpProviderSessions.layer),
    ),
  );

describe("Cursor interrupted prompt evidence", () => {
  it.effect("R3 different native agent ignores the pending carry", () =>
    provide(
      Effect.gen(function* () {
        const f = yield* setup("R3");
        yield* f.runtime.startTurn(f.turn("R3 request A: write a dog story."));
        yield* f.interrupt();
        const agentB = yield* f.newThread();
        yield* f.runtime.startTurn(f.turn("R3 request B: inspect the garden.", [], agentB));
        yield* f.finish();
        assert.notEqual(f.sent[0]!.agent, f.sent[1]!.agent);
        assert.deepEqual(
          f.sent.map((s) => s.text),
          [
            outbound("R3 request A: write a dog story."),
            outbound("R3 request B: inspect the garden."),
          ],
        );
      }),
    ),
  );

  it.effect("R4 compress sends no carry and preserves it for the next real message", () =>
    provide(
      Effect.gen(function* () {
        const f = yield* setup("R4");
        const first = "R4 request A: write a dog story.";
        const next = "R4 request B: make the dog a pig.";
        yield* f.runtime.startTurn(f.turn(first));
        yield* f.interrupt();
        assert(f.runtime.compactThread !== undefined);
        yield* f.runtime.compactThread(f.turn("/compress"));
        yield* f.finish();
        yield* f.runtime.startTurn(f.turn(next));
        yield* f.finish();
        assert.deepEqual(
          f.sent.map((s) => s.summarize),
          [false, true, false],
        );
        assert.deepEqual(
          f.sent.map((s) => s.text),
          [outbound(first), "/compress", outbound(carry(first, next))],
        );
      }),
    ),
  );

  it.effect("R5 rejected send preserves carry for retry and successful completion clears it", () =>
    provide(
      Effect.gen(function* () {
        const f = yield* setup("R5");
        const first = "R5 request A: write a dog story.";
        const next = "R5 request B: make the dog a pig.";
        const last = "R5 request C: now write about a cow.";
        yield* f.runtime.startTurn(f.turn(first));
        yield* f.interrupt();
        f.rejectNextSend();
        const rejected = yield* f.runtime.startTurn(f.turn(next)).pipe(Effect.flip);
        assert.equal(rejected._tag, "ProviderAdapterTurnStartError");
        yield* f.runtime.startTurn(f.turn(next));
        yield* f.finish();
        yield* f.runtime.startTurn(f.turn(last));
        yield* f.finish();
        assert.deepEqual(
          f.sent.map((s) => s.accepted),
          [true, false, true, true],
        );
        assert.deepEqual(
          f.sent.map((s) => s.text),
          [
            outbound(first),
            outbound(carry(first, next)),
            outbound(carry(first, next)),
            outbound(last),
          ],
        );
      }),
    ),
  );

  it.effect("R6 repeated steers chain both earlier requests into the third send", () =>
    provide(
      Effect.gen(function* () {
        const f = yield* setup("R6");
        const first = "R6 request A: write a dog story.";
        const second = "R6 request B: make the dog a pig.";
        const third = "R6 request C: put the pig on the moon.";
        yield* f.runtime.startTurn(f.turn(first));
        yield* f.interrupt();
        yield* f.runtime.startTurn(f.turn(second));
        yield* f.interrupt();
        yield* f.runtime.startTurn(f.turn(third));
        yield* f.finish();
        assert.deepEqual(
          f.sent.map((s) => s.text),
          [
            outbound(first),
            outbound(carry(first, second)),
            outbound(carry(carry(first, second), third)),
          ],
        );
      }),
    ),
  );

  it.effect(
    "R8 interrupted native images carry text and saved paths without resending image bytes",
    () =>
      provide(
        Effect.gen(function* () {
          const f = yield* setup("R8");
          const first = "R8 request A: compare these two images.";
          const next = "R8 request B: focus on the first image.";
          const imageData =
            "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+j1ioAAAAASUVORK5CYII=";
          const attachments = ["first", "second"].map((name) =>
            ChatImageAttachment.make({
              type: "image",
              id: ChatAttachmentId.make(`evidence-image-${name}`),
              name: `${name}.png`,
              mimeType: "image/png",
              sizeBytes: Buffer.from(imageData, "base64").length,
            }),
          );
          const paths = attachments.map((attachment) => f.host.resolveAttachmentPath(attachment)!);
          yield* Effect.forEach(paths, (path) =>
            f.fs.writeFile(path, Buffer.from(imageData, "base64")),
          );
          const original = `${first}\n\n[Attached image "first.png" is saved at: ${paths[0]}]\n\n[Attached image "second.png" is saved at: ${paths[1]}]`;
          yield* f.runtime.startTurn(f.turn(first, attachments));
          yield* f.interrupt();
          yield* f.runtime.startTurn(f.turn(next));
          yield* f.finish();
          assert.deepEqual(
            f.sent.map((s) => s.images),
            [
              [
                { data: imageData, mimeType: "image/png" },
                { data: imageData, mimeType: "image/png" },
              ],
              [],
            ],
          );
          assert.deepEqual(
            f.sent.map((s) => s.text),
            [outbound(original), outbound(carry(original, next))],
          );
        }),
      ),
  );
});
