import * as ClaudeSdk from "@anthropic-ai/claude-agent-sdk";
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { vi } from "vite-plus/test";
import { assert, it } from "@effect/vitest";
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { ClaudeSettings, MessageId, NodeId, ProjectId, ProviderInstanceId, ProviderSessionId, RunAttemptId, RunId, ThreadId, type ModelSelection, type OrchestrationV2AppThread, type OrchestrationV2ProviderThread } from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
import * as SecureRandom from "effect/crypto/SecureRandom";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Queue from "effect/Queue";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
import * as ClaudeAdapterV2 from "./ClaudeAdapterV2.ts";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => ({
 ...(await importOriginal<typeof ClaudeSdk>()), resolveSettings: vi.fn(async () => ({effective:{},provenance:{},sources:[]})),
}));
const DEFAULT_CLAUDE_SETTINGS=Schema.decodeSync(ClaudeSettings)({customModels:["synthetic-fixture"]});
const CLAUDE_TEST_MODEL_SELECTION={instanceId:ProviderInstanceId.make("claudeAgent"),model:"synthetic-fixture"};
const CLAUDE_TEST_RUNTIME_POLICY=ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({runtimeMode:"approval-required",interactionMode:"default",cwd:null});
function makeClaudeTestAppThread(input: {
  readonly threadId: ThreadId;
  readonly providerThread: OrchestrationV2ProviderThread;
  readonly now: DateTime.Utc;
}): OrchestrationV2AppThread {
  return {
    createdBy: "user",
    creationSource: "web",
    id: input.threadId,
    projectId: ProjectId.make(`project-${input.threadId}`),
    title: "Claude attachment test",
    providerInstanceId: ProviderInstanceId.make(ClaudeAdapterV2.CLAUDE_PROVIDER),
    modelSelection: CLAUDE_TEST_MODEL_SELECTION,
    runtimeMode: "full-access",
    interactionMode: "default",
    branch: null,
    worktreePath: null,
    activeProviderThreadId: input.providerThread.id,
    lineage: {
      parentThreadId: null,
      relationshipToParent: null,
      rootThreadId: input.threadId,
    },
    forkedFrom: null,
    createdAt: input.now,
    updatedAt: input.now,
    archivedAt: null,
    settledOverride: null,
    settledAt: null,
    lastVisitedAt: null,
    deletedAt: null,
  };
}

function makeClaudeTestTurnInput(input: {
  readonly threadId: ThreadId;
  readonly providerThread: OrchestrationV2ProviderThread;
  readonly now: DateTime.Utc;
  readonly attemptId: RunAttemptId;
  readonly text: string;
  readonly attachments: ProviderAdapter.ProviderAdapterV2TurnInput["message"]["attachments"];
  readonly providerTurnOrdinal?: number;
  readonly nativeThreadHasTurns?: boolean;
  readonly messageCreatedBy?: ProviderAdapter.ProviderAdapterV2TurnInput["message"]["createdBy"];
  readonly messageCreationSource?: ProviderAdapter.ProviderAdapterV2TurnInput["message"]["creationSource"];
  readonly modelSelection?: ModelSelection;
  readonly runtimePolicy?: ProviderAdapter.ProviderAdapterV2RuntimePolicy;
}): ProviderAdapter.ProviderAdapterV2TurnInput {
  return {
    appThread: makeClaudeTestAppThread(input),
    threadId: input.threadId,
    runId: RunId.make(`run-${input.attemptId}`),
    runOrdinal: 1,
    providerTurnOrdinal: input.providerTurnOrdinal ?? 1,
    ...(input.nativeThreadHasTurns === undefined
      ? {}
      : { nativeThreadHasTurns: input.nativeThreadHasTurns }),
    attemptId: input.attemptId,
    rootNodeId: NodeId.make(`node-${input.attemptId}`),
    providerThread: input.providerThread,
    message: {
      createdBy: input.messageCreatedBy ?? "user",
      creationSource: input.messageCreationSource ?? "web",
      messageId: MessageId.make(`message-${input.attemptId}`),
      text: input.text,
      attachments: input.attachments,
    },
    modelSelection: input.modelSelection ?? CLAUDE_TEST_MODEL_SELECTION,
    runtimePolicy: input.runtimePolicy ?? CLAUDE_TEST_RUNTIME_POLICY,
  };
}

  const openAdapterWhoseFirstQueryEndsBeforeItsFirstFrame = Effect.fnUntraced(function* (
    name: string,
  ) {
    const fileSystem = yield* FileSystem.FileSystem;
    const idAllocator = yield* IdAllocator.IdAllocatorV2;
    const attachmentsDir = yield* fileSystem.makeTempDirectoryScoped({
      prefix: `t3-claude-v2-${name}-`,
    });
    const deadStreamEnded = yield* Deferred.make<void>();
    let opened = 0;
    const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({
      instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
      settings: DEFAULT_CLAUDE_SETTINGS,
      environment: {},
      attachmentsDir,
      // startTurn reads the auto-compact window from settings between opening the query and
      // attaching the turn; holding that read until the CLI's stream has ended pins the race.
      fileSystem: {
        ...fileSystem,
        readFileString: (filePath, encoding) =>
          opened === 1 && filePath.endsWith("settings.json")
            ? Deferred.await(deadStreamEnded).pipe(
                Effect.andThen(fileSystem.readFileString(filePath, encoding)),
              )
            : fileSystem.readFileString(filePath, encoding),
      },
      path: yield* Path.Path,
      random: yield* SecureRandom.SecureRandom,
      idAllocator,
      continuationRequests: { offer: () => Effect.void },
      queryRunner: {
        allocateSessionId: Effect.succeed(`native-${name}`),
        open: () =>
          Effect.gen(function* () {
            opened += 1;
            const sdkMessages = yield* Queue.unbounded<SDKMessage>();
            return {
              // The first CLI exits without writing a frame.
              messages:
                opened === 1
                  ? Stream.empty.pipe(Stream.ensuring(Deferred.succeed(deadStreamEnded, undefined).pipe(Effect.tap(() => Effect.sync(() => console.log("HARNESS: empty stream ended; releasing settings read before continuation claim"))))))
                  : Stream.fromQueue(sdkMessages),
              offer: () => Effect.die("HARNESS: continuation unexpectedly offered a user prompt"),
              setModel: () => Effect.void,
              applyFlagSettings: () => Effect.void,
              setPermissionMode: () => Effect.void,
              backgroundTasks: () => Effect.succeed(false),
              mcpServerStatus: Effect.succeed([]),
              readMcpResource: () => Effect.die("unexpected MCP resource read"),
              callMcpTool: () => Effect.die("unexpected MCP tool call"),
              listPermissionRules: Effect.die("unexpected permission rules read"),
              stopTask: () => Effect.void,
              interrupt: Effect.void,
              close: Queue.shutdown(sdkMessages),
            };
          }),
        forkSession: () => Effect.die("unused forkSession"),
        subagentLaunchToolUseId: () => Effect.succeed(null),
        assertComplete: Effect.void,
      },
    });
    const threadId = ThreadId.make(`thread-${name}`);
    const runtime = yield* adapter.openSession({
      threadId,
      providerSessionId: ProviderSessionId.make(`provider-session-${name}`),
      modelSelection: CLAUDE_TEST_MODEL_SELECTION,
      runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
    });
    const providerThread = yield* runtime.ensureThread({
      threadId,
      modelSelection: CLAUDE_TEST_MODEL_SELECTION,
      runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY,
    });
    const now = yield* DateTime.now;
    const turnInput = (attempt: string, providerTurnOrdinal: number) =>
      makeClaudeTestTurnInput({
        threadId,
        providerThread,
        now,
        attemptId: RunAttemptId.make(`attempt-${name}-${attempt}`),
        text: "Synthetic provider continuation.",
        messageCreatedBy: attempt === "continuation" ? "agent" : "user",
        messageCreationSource: attempt === "continuation" ? "provider" : "web",
        attachments: [],
        providerTurnOrdinal,
      });
    return { runtime, turnInput };
  });


it.effect("synthetic continuation pre-claim race: Deferred pins stream exit before attachment", () =>
 Effect.scoped(Effect.gen(function* () {
  console.log("HARNESS ONLY: synthetic continuation provenance; no browser or orchestrator wake-routing claim");
  const h=yield* openAdapterWhoseFirstQueryEndsBeforeItsFirstFrame("synthetic-continuation");
  const input=h.turnInput("continuation",2);
  assert.equal(input.message.createdBy,"agent");
  assert.equal(input.message.creationSource,"provider");
  console.log("HARNESS: createdBy=agent, creationSource=provider; no user prompt.offer allowed");
  const exit=yield* h.runtime.startTurn(input).pipe(Effect.exit);
  assert.isTrue(Exit.isFailure(exit));
  if(Exit.isFailure(exit)) {
   const cause=Cause.pretty(exit.cause);
   assert.include(cause,"Claude provider query ended before the turn could start.");
   console.log("HARNESS: continuation rejected with exact cause: Claude provider query ended before the turn could start.");
  }
 }).pipe(Effect.provide(Layer.mergeAll(IdAllocator.layer,McpProviderSessions.layer,NodeServices.layer,NodeCrypto.layer)))));
