import { describe, expect, it } from "vitest";

import {
  runHistorySyncOperation,
  type HistorySyncCommand,
} from "@/features/agents/operations/historySyncOperation";
import type { AgentState } from "@/features/agents/state/store";

type ChatHistoryMessage = Record<string, unknown>;

const createAgent = (overrides?: Partial<AgentState>): AgentState => {
  const base: AgentState = {
    agentId: "agent-1",
    name: "Agent One",
    sessionKey: "agent:agent-1:main",
    status: "idle",
    sessionCreated: true,
    awaitingUserInput: false,
    hasUnseenActivity: false,
    outputLines: [],
    lastResult: null,
    lastDiff: null,
    runId: null,
    runStartedAt: null,
    streamText: null,
    thinkingTrace: null,
    latestOverride: null,
    latestOverrideKind: null,
    lastAssistantMessageAt: null,
    lastActivityAt: null,
    latestPreview: null,
    lastUserMessage: null,
    draft: "",
    sessionSettingsSynced: true,
    historyLoadedAt: null,
    historyFetchLimit: null,
    historyFetchedCount: null,
    historyMaybeTruncated: false,
    toolCallingEnabled: true,
    showThinkingTraces: true,
    model: "openai/gpt-5",
    thinkingLevel: "medium",
    avatarSeed: "seed-1",
    avatarUrl: null,
  };
  return { ...base, ...(overrides ?? {}) };
};

const getCommandsByKind = <TKind extends HistorySyncCommand["kind"]>(
  commands: HistorySyncCommand[],
  kind: TKind
): Array<Extract<HistorySyncCommand, { kind: TKind }>> =>
  commands.filter((command) => command.kind === kind) as Array<
    Extract<HistorySyncCommand, { kind: TKind }>
  >;

describe("historySyncOperation", () => {
  it("returns noop when request intent resolves to skip", async () => {
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() => ({ messages: [] as ChatHistoryMessage[] }) as T,
      },
      agentId: "agent-1",
      getAgent: () => null,
      inFlightSessionKeys: new Set<string>(),
      requestId: "req-1",
      loadedAt: 1_234,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: true,
    });

    expect(commands).toEqual([{ kind: "noop", reason: "missing-agent" }]);
  });

  it("applies history updates even when latest agent is running with active run", async () => {
    const agent = createAgent({
      status: "running",
      runId: "run-1",
      transcriptRevision: 3,
      outputLines: ["> local question", "assistant draft"],
    });
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() =>
          ({
            sessionKey: agent.sessionKey,
            messages: [{ role: "assistant", content: "remote answer" }],
          }) as T,
      },
      agentId: "agent-1",
      getAgent: () => agent,
      inFlightSessionKeys: new Set<string>(),
      requestId: "req-2",
      loadedAt: 2_345,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: true,
    });

    const updates = getCommandsByKind(commands, "dispatchUpdateAgent");
    const metrics = getCommandsByKind(commands, "logMetric");
    expect(metrics).toEqual([]);

    const finalUpdate = updates[updates.length - 1];
    if (!finalUpdate) throw new Error("Expected final update command.");
    const patch = finalUpdate.patch;
    expect(patch.outputLines).toContain("> local question");
    expect(patch.outputLines).toContain("assistant draft");
    expect(patch.outputLines).toContain("remote answer");
    expect(patch.lastResult).toBe("remote answer");
    expect(patch.latestPreview).toBe("remote answer");
    expect(patch.lastAppliedHistoryRequestId).toBe("req-2");
  });

  it("returns transcript merge update commands when disposition is apply and transcript v2 is enabled", async () => {
    const agent = createAgent({
      transcriptRevision: 1,
      outputLines: ["> local question"],
    });
    const markdownAssistant = [
      "- first bullet",
      "- second bullet",
      "",
      "```ts",
      "console.log('merged answer');",
      "```",
    ].join("\n");
    const messages: ChatHistoryMessage[] = [{ role: "assistant", content: markdownAssistant }];
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() =>
          ({
            sessionKey: agent.sessionKey,
            messages,
          }) as T,
      },
      agentId: "agent-1",
      getAgent: () => agent,
      inFlightSessionKeys: new Set<string>(),
      requestId: "req-3",
      loadedAt: 3_456,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: true,
    });

    const updates = getCommandsByKind(commands, "dispatchUpdateAgent");
    expect(updates.length).toBeGreaterThanOrEqual(2);
    expect(updates).toContainEqual({
      kind: "dispatchUpdateAgent",
      agentId: "agent-1",
      patch: { lastHistoryRequestRevision: 1 },
    });
    const finalUpdate = updates[updates.length - 1];
    if (!finalUpdate) throw new Error("Expected final update command.");
    const patch = finalUpdate.patch;
    expect(Array.isArray(patch.outputLines)).toBe(true);
    expect(patch.outputLines).toContain("> local question");
    expect(patch.outputLines).toContain(markdownAssistant);
    expect(patch.lastResult).toBe(markdownAssistant);
    expect(patch.latestPreview).toBe(markdownAssistant);
    expect(patch.lastAppliedHistoryRequestId).toBe("req-3");
  });

  it("normalizes assistant text in transcript-v2 history sync patches", async () => {
    const agent = createAgent({
      transcriptRevision: 1,
      outputLines: ["> local question"],
    });
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() =>
          ({
            sessionKey: agent.sessionKey,
            messages: [{ role: "assistant", content: "\n- alpha  \n\n\n- beta\t \n\n" }],
          }) as T,
      },
      agentId: "agent-1",
      getAgent: () => agent,
      inFlightSessionKeys: new Set<string>(),
      requestId: "req-3b",
      loadedAt: 3_789,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: true,
    });

    const updates = getCommandsByKind(commands, "dispatchUpdateAgent");
    const finalUpdate = updates[updates.length - 1];
    if (!finalUpdate) throw new Error("Expected final update command.");
    const patch = finalUpdate.patch;
    expect(Array.isArray(patch.outputLines)).toBe(true);
    expect(patch.outputLines).toContain("> local question");
    expect(patch.outputLines).toContain("- alpha\n\n- beta");
    expect(patch.lastResult).toBe("- alpha\n\n- beta");
    expect(patch.latestPreview).toBe("- alpha\n\n- beta");
    expect(patch.lastAppliedHistoryRequestId).toBe("req-3b");
  });

  it("infers running state from recent user-terminal history in transcript-v2 mode", async () => {
    const agent = createAgent({
      status: "idle",
      runId: null,
      runStartedAt: null,
      transcriptRevision: 1,
      outputLines: [],
    });
    const loadedAt = Date.parse("2024-01-01T00:30:00.000Z");
    const lastUserAt = Date.parse("2024-01-01T00:29:40.000Z");
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() =>
          ({
            sessionKey: agent.sessionKey,
            messages: [
              {
                role: "user",
                timestamp: new Date(lastUserAt).toISOString(),
                content: "still working?",
              },
            ],
          }) as T,
      },
      agentId: "agent-1",
      getAgent: () => agent,
      inFlightSessionKeys: new Set<string>(),
      requestId: "req-3c",
      loadedAt,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: true,
    });

    const updates = getCommandsByKind(commands, "dispatchUpdateAgent");
    const finalUpdate = updates[updates.length - 1];
    if (!finalUpdate) throw new Error("Expected final update command.");
    const patch = finalUpdate.patch;
    expect(patch.status).toBe("running");
    expect(patch.runId).toBeNull();
    expect(patch.runStartedAt).toBe(lastUserAt);
    expect(patch.streamText).toBeNull();
    expect(patch.thinkingTrace).toBeNull();
    expect(patch.lastUserMessage).toBe("still working?");
    expect(patch.lastAppliedHistoryRequestId).toBe("req-3c");
  });

  it("does not infer running state when terminal history includes an aborted assistant message", async () => {
    const agent = createAgent({
      status: "idle",
      runId: null,
      runStartedAt: null,
      transcriptRevision: 1,
      outputLines: [],
    });
    const loadedAt = Date.parse("2024-01-01T00:30:00.000Z");
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() =>
          ({
            sessionKey: agent.sessionKey,
            messages: [
              {
                role: "user",
                timestamp: "2024-01-01T00:29:40.000Z",
                content: "still working?",
              },
              {
                role: "assistant",
                timestamp: "2024-01-01T00:29:41.000Z",
                content: [],
                stopReason: "aborted",
                errorMessage: "Request was aborted",
              },
            ],
          }) as T,
      },
      agentId: "agent-1",
      getAgent: () => agent,
      inFlightSessionKeys: new Set<string>(),
      requestId: "req-3d",
      loadedAt,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: true,
    });

    const updates = getCommandsByKind(commands, "dispatchUpdateAgent");
    const finalUpdate = updates[updates.length - 1];
    if (!finalUpdate) throw new Error("Expected final update command.");
    const patch = finalUpdate.patch;
    expect(patch.status).toBeUndefined();
    expect(patch.runId).toBeUndefined();
    expect(Array.isArray(patch.outputLines)).toBe(true);
    expect(patch.outputLines).toContain("Run aborted.");
    expect(patch.lastResult).toBe("Run aborted.");
    expect(patch.latestPreview).toBe("Run aborted.");
    expect(patch.lastAppliedHistoryRequestId).toBe("req-3d");
  });

  it("returns legacy history sync patch command when transcript v2 is disabled", async () => {
    const agent = createAgent({
      transcriptRevision: 0,
      outputLines: ["> local question"],
    });
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() =>
          ({
            sessionKey: agent.sessionKey,
            messages: [{ role: "assistant", content: "Legacy answer" }],
          }) as T,
      },
      agentId: "agent-1",
      getAgent: () => agent,
      inFlightSessionKeys: new Set<string>(),
      requestId: "req-4",
      loadedAt: 4_567,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: false,
    });

    const updates = getCommandsByKind(commands, "dispatchUpdateAgent");
    const finalUpdate = updates[updates.length - 1];
    if (!finalUpdate) throw new Error("Expected final update command.");
    const patch = finalUpdate.patch;
    expect(patch.outputLines).toContain("> local question");
    expect(patch.outputLines).toContain("Legacy answer");
    expect(patch.lastResult).toBe("Legacy answer");
    expect(patch.lastAppliedHistoryRequestId).toBe("req-4");
  });

  it("drops stale history when transcript revision changes during fetch", async () => {
    const requestAgent = createAgent({
      transcriptRevision: 7,
      outputLines: ["> local question", "assistant current"],
    });
    const latestAgent = createAgent({
      transcriptRevision: 8,
      outputLines: ["> local question", "assistant current"],
    });
    let readCount = 0;
    const inFlight = new Set<string>();
    const commands = await runHistorySyncOperation({
      client: {
        call: async <T>() =>
          ({
            sessionKey: requestAgent.sessionKey,
            messages: [{ role: "assistant", content: "stale remote answer" }],
          }) as T,
      },
      agentId: "agent-1",
      getAgent: () => {
        readCount += 1;
        return readCount <= 1 ? requestAgent : latestAgent;
      },
      inFlightSessionKeys: inFlight,
      requestId: "req-5",
      loadedAt: 5_678,
      defaultLimit: 200,
      maxLimit: 5000,
      transcriptV2Enabled: true,
    });

    const metrics = getCommandsByKind(commands, "logMetric");
    expect(metrics).toEqual([
      {
        kind: "logMetric",
        metric: "history_response_dropped_stale",
        meta: {
          reason: "transcript_revision_changed",
          agentId: "agent-1",
          requestId: "req-5",
        },
      },
    ]);

    const updates = getCommandsByKind(commands, "dispatchUpdateAgent");
    expect(updates).toContainEqual({
      kind: "dispatchUpdateAgent",
      agentId: "agent-1",
      patch: { lastHistoryRequestRevision: 7 },
    });
    const finalUpdate = updates[updates.length - 1];
    if (!finalUpdate) throw new Error("Expected final update command.");
    const patch = finalUpdate.patch;
    expect(patch).not.toHaveProperty("outputLines");
    expect(patch).not.toHaveProperty("lastAppliedHistoryRequestId");
    expect(patch).not.toHaveProperty("lastResult");
    expect(patch).not.toHaveProperty("latestPreview");
    expect(inFlight.size).toBe(0);
  });
});
