import { afterEach, describe, expect, it } from "vitest";
import { rmSync } from "node:fs";

if (!(globalThis as any).document) {
  const { Window } = await import("happy-dom");
  const window = new Window();
  (globalThis as any).window = window;
  (globalThis as any).document = window.document;
  Object.defineProperty(globalThis, "navigator", {
    configurable: true,
    value: window.navigator,
  });
  (globalThis as any).Node = window.Node;
  (globalThis as any).Element = window.Element;
  (globalThis as any).HTMLElement = window.HTMLElement;
  (globalThis as any).SVGElement = window.SVGElement;
  (globalThis as any).Event = window.Event;
  (globalThis as any).CustomEvent = window.CustomEvent;
  (globalThis as any).MouseEvent = window.MouseEvent;
  (globalThis as any).KeyboardEvent = window.KeyboardEvent;
  (globalThis as any).getComputedStyle = window.getComputedStyle.bind(window);
}

globalThis.requestAnimationFrame = ((cb: FrameRequestCallback) => {
  cb(0);
  return 1;
}) as typeof globalThis.requestAnimationFrame;
globalThis.cancelAnimationFrame = (() => {}) as typeof globalThis.cancelAnimationFrame;

const { nextTick } = await import("vue");
const { createTerminalApp } = await import("@simon_he/vue-tui");
const { DimSdkShell, createDimSdkRouter } = await import("../src/dimsdk/index.ts");
const { createGoatChainBridge } = await import("../src/best-agent-native-bridge.ts");

async function flushApp(app: {
  scheduler: { flush: () => void };
}) {
  await nextTick();
  await nextTick();
  app.scheduler.flush();
}

async function sleep(ms: number) {
  await new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
}

function snapshotText(app: {
  terminal: { snapshot: () => { lines: readonly string[] } };
}): string {
  return app.terminal.snapshot().lines.join("\n");
}

async function waitFor<T>(
  fn: () => T | null | undefined,
  message: string,
  advance: () => Promise<void>,
  tries = 80,
  debug?: () => string,
): Promise<T> {
  for (let index = 0; index < tries; index += 1) {
    const value = fn();
    if (value) return value;
    await advance();
  }
  const detail = debug?.();
  throw new Error(detail ? `${message}\n\n${detail}` : message);
}

afterEach(() => {
  delete (globalThis as any).__BEST_AGENT_OPENAI_PROVIDER__;
});

describe("dimsdk final message reconciliation", () => {
  it("renders the final assistant item even when earlier streamed text differs", async () => {
    const threadId = "thread_stream_then_final_item";
    const turnId = "turn_stream_then_final_item";
    const streamedText = "让我先获取仓库当前结构的最新信息。";
    const finalText = "# 最终分析报告\n\n这里是最终结论。";
    const listeners = new Set<(envelope: { event: unknown; sequence: number }) => void>();
    const envelopes: Array<{ event: unknown; sequence: number }> = [];
    let sequence = 0;
    let storeRef: any;

    const thread = {
      assetCount: 0,
      childCount: 0,
      createdAt: "2026-04-15T03:07:02.000Z",
      cwd: process.cwd(),
      id: threadId,
      mode: "default",
      preview: "",
      status: "active",
      title: "stream then final item",
      turnCount: 0,
      updatedAt: "2026-04-15T03:07:02.000Z",
    };

    const emit = (event: unknown) => {
      sequence += 1;
      const envelope = { event, sequence };
      envelopes.push(envelope);
      for (const listener of listeners)
        listener(envelope);
    };

    const runtime = {
      async forkThread() {
        return { thread };
      },
      async getThread() {
        return { thread };
      },
      async getThreadEvents(input: { afterSequence?: number; threadId: string }) {
        const afterSequence = Math.max(0, Number(input.afterSequence ?? 0));
        const events = envelopes.filter((envelope) => {
          const record = envelope.event as { threadId?: string; turn?: { threadId?: string } };
          const eventThreadId = record.threadId ?? record.turn?.threadId;
          return eventThreadId === input.threadId && envelope.sequence > afterSequence;
        });
        return {
          events,
          hasMore: false,
          latestSequence: envelopes.at(-1)?.sequence ?? 0,
          nextAfterSequence: events.at(-1)?.sequence,
          threadId: input.threadId,
        };
      },
      async interruptTurn() {},
      async listThreads() {
        return { threads: [thread] };
      },
      async purgeThread() {},
      async resumeThread() {
        return { items: [], thread };
      },
      async runTurn(params: { threadId: string }) {
        setTimeout(() => {
          void (async () => {
            emit({
              turn: {
                createdAt: "2026-04-15T03:07:02.419Z",
                id: turnId,
                status: "running",
                threadId: params.threadId,
              },
              type: "turn.started",
            });
            emit({
              delta: streamedText,
              streamId: "text-compatible",
              threadId: params.threadId,
              turnId,
              type: "turn.text.delta",
            });
            emit({
              callId: "call_read_file",
              threadId: params.threadId,
              toolName: "read-file",
              turnId,
              type: "turn.tool-call.started",
            });
            emit({
              callId: "call_read_file",
              delta: "{\"path\":\"package.json\"}",
              threadId: params.threadId,
              toolName: "read-file",
              turnId,
              type: "turn.tool-call.delta",
            });
            emit({
              item: {
                callId: "call_read_file",
                createdAt: "2026-04-15T03:07:02.500Z",
                id: "item_tool_call_read",
                input: "{\"path\":\"package.json\"}",
                kind: "tool-call",
                threadId: params.threadId,
                toolName: "read-file",
                turnId,
              },
              turnId,
              type: "turn.item.added",
            });
            emit({
              item: {
                callId: "call_read_file",
                createdAt: "2026-04-15T03:07:02.650Z",
                id: "item_tool_result_read",
                kind: "tool-result",
                output: "=== file: package.json ===\\n{\\n  \\\"name\\\": \\\"best-agent\\\"\\n}",
                threadId: params.threadId,
                toolName: "read-file",
                turnId,
              },
              turnId,
              type: "turn.item.added",
            });
            await sleep(10);
            emit({
              item: {
                createdAt: "2026-04-15T03:07:03.000Z",
                id: "item_final_agent_message",
                kind: "message",
                role: "agent",
                text: finalText,
                threadId: params.threadId,
                turnId,
              },
              turnId,
              type: "turn.item.added",
            });
            emit({
              turn: {
                createdAt: "2026-04-15T03:07:02.419Z",
                id: turnId,
                status: "completed",
                threadId: params.threadId,
              },
              type: "turn.completed",
            });
          })();
        }, 0);
        return {
          turn: {
            createdAt: "2026-04-15T03:07:02.419Z",
            id: turnId,
            status: "running",
            threadId: params.threadId,
          },
        };
      },
      async setThreadMode() {
        return { thread };
      },
      async startThread() {
        return { thread };
      },
      subscribe(listener: (envelope: { event: unknown; sequence: number }) => void) {
        listeners.add(listener);
        return () => listeners.delete(listener);
      },
      async unarchiveThread() {},
    };

    const bridge = createGoatChainBridge({ runtime: runtime as never });

    const app = createTerminalApp({
      cols: 120,
      rows: 36,
      component: DimSdkShell as never,
      props: {
        toolRunner: async () => ({
          id: "tool",
          status: "error" as const,
          output: "Tool execution stays inside best-agent.",
        }),
        context: {
          system: "You are best-agent running in a final-message reconciliation test.",
          cwd: process.cwd(),
          env: {
            ...process.env,
            DIMCODE_VERSION: process.env.DIMCODE_VERSION ?? "0.0.0",
          },
        },
        agent: bridge.agent,
        onStoreCreated: (store: any) => {
          storeRef = store;
          bridge.attachStore(store);
          store.setCwd?.(process.cwd());
          store.state.sessionId = threadId;
          store.state.messages = [];
        },
        sessions: bridge.sessions,
      },
      defaultStyle: { bg: "black", fg: "whiteBright" },
    });

    try {
      app.app.use(createDimSdkRouter({ initialRoute: "chat", mode: "cli" }) as never);
      app.mount();
      await flushApp(app);

      const store = await waitFor(
        () => storeRef,
        "DimSdk store was not created for the final-message reconciliation test.",
        () => flushApp(app),
      );

      void store.sendUserInput({ content: "分析仓库" });

      await waitFor(
        () => {
          const assistant = store.state.messages.find((message: any) => message.role === "assistant");
          if (!assistant)
            return null;
          const markdownParts = assistant.parts.filter((part: any) => part.type === "markdown");
          if (markdownParts.length !== 1)
            return null;
          const markdown = String(markdownParts[0]?.markdown ?? "");
          return markdown === finalText ? assistant : null;
        },
        "Final assistant item did not replace the provisional streamed text.",
        async () => {
          await sleep(10);
          await flushApp(app);
        },
        120,
        () => `${snapshotText(app)}\n\n${JSON.stringify(store.state.messages, null, 2)}`,
      );

      const assistant = store.state.messages.find((message: any) => message.role === "assistant");
      expect(assistant).toBeDefined();
      if (!assistant)
        throw new Error("assistant message missing");

      const markdownParts = assistant.parts.filter((part: any) => part.type === "markdown");
      expect(markdownParts).toHaveLength(1);
      expect(String(markdownParts[0]?.markdown ?? "")).toBe(finalText);
      const normalizedSnapshot = snapshotText(app).replace(/ /g, "");
      expect(normalizedSnapshot).toContain("最终分析报告");
      expect(normalizedSnapshot).not.toContain(streamedText);
    } finally {
      app.dispose();
    }
  });
});
