/**
 * Unit tests for message-adapter.ts — parseMessage for all 3 providers.
 */
import { describe, it, expect } from "bun:test";
import { parseMessage, type AgentMessage, type ProviderId } from "../message-adapter.ts";

// ── Helpers ─────────────────────────────────────────────────────────────────

function firstOf(msgs: AgentMessage[]): AgentMessage {
  expect(msgs.length).toBeGreaterThanOrEqual(1);
  return msgs[0];
}

function contentOf(msg: AgentMessage): Record<string, unknown> {
  return msg.content as Record<string, unknown>;
}

// ── Claude adapter ──────────────────────────────────────────────────────────

describe("Claude adapter", () => {
  it("parses system/init event", () => {
    const msg = firstOf(
      parseMessage("claude", {
        type: "system",
        subtype: "init",
        session_id: "sess-123",
        model: "opus-4",
        cwd: "/home/user/project",
        tools: ["Bash", "Read"],
      })
    );
    expect(msg.type).toBe("init");
    const c = contentOf(msg);
    expect(c.sessionId).toBe("sess-123");
    expect(c.model).toBe("opus-4");
    expect(c.cwd).toBe("/home/user/project");
    expect(c.tools).toEqual(["Bash", "Read"]);
  });

  it("parses system/compact_boundary event", () => {
    const msg = firstOf(
      parseMessage("claude", {
        type: "system",
        subtype: "compact_boundary",
        compact_metadata: { trigger: "auto", pre_tokens: 50000 },
      })
    );
    expect(msg.type).toBe("compaction");
    const c = contentOf(msg);
    expect(c.trigger).toBe("auto");
    expect(c.preTokens).toBe(50000);
  });

  it("parses assistant text block", () => {
    const msgs = parseMessage("claude", {
      type: "assistant",
      uuid: "u1",
      message: {
        content: [{ type: "text", text: "Hello world" }],
      },
    });
    const msg = firstOf(msgs);
    expect(msg.type).toBe("text");
    expect(contentOf(msg).text).toBe("Hello world");
  });

  it("parses assistant thinking block", () => {
    const msgs = parseMessage("claude", {
      type: "assistant",
      uuid: "u1",
      message: {
        content: [{ type: "thinking", thinking: "Let me consider..." }],
      },
    });
    expect(firstOf(msgs).type).toBe("thinking");
    expect(contentOf(firstOf(msgs)).text).toBe("Let me consider...");
  });

  it("parses assistant tool_use block", () => {
    const msgs = parseMessage("claude", {
      type: "assistant",
      uuid: "u1",
      message: {
        content: [
          {
            type: "tool_use",
            id: "tu-1",
            name: "Bash",
            input: { command: "ls -la" },
          },
        ],
      },
    });
    const msg = firstOf(msgs);
    expect(msg.type).toBe("tool_call");
    const c = contentOf(msg);
    expect(c.toolUseId).toBe("tu-1");
    expect(c.name).toBe("Bash");
    expect((c.input as Record<string, unknown>).command).toBe("ls -la");
  });

  it("parses user/tool_result block", () => {
    const msgs = parseMessage("claude", {
      type: "user",
      uuid: "u2",
      message: {
        content: [
          {
            type: "tool_result",
            tool_use_id: "tu-1",
            content: "file1.txt\nfile2.txt",
          },
        ],
      },
    });
    const msg = firstOf(msgs);
    expect(msg.type).toBe("tool_result");
    expect(contentOf(msg).toolUseId).toBe("tu-1");
  });

  it("parses result/cost event", () => {
    const msg = firstOf(
      parseMessage("claude", {
        type: "result",
        uuid: "u3",
        total_cost_usd: 0.42,
        duration_ms: 15000,
        usage: { input_tokens: 1000, output_tokens: 500 },
        num_turns: 3,
        is_error: false,
        result: "Task completed",
      })
    );
    expect(msg.type).toBe("cost");
    const c = contentOf(msg);
    expect(c.totalCostUsd).toBe(0.42);
    expect(c.durationMs).toBe(15000);
    expect(c.inputTokens).toBe(1000);
    expect(c.outputTokens).toBe(500);
    expect(c.numTurns).toBe(3);
    expect(c.isError).toBe(false);
    expect(c.result).toBe("Task completed");
  });

  it("returns empty for assistant with no content", () => {
    const msgs = parseMessage("claude", {
      type: "assistant",
      message: {},
    });
    expect(msgs).toHaveLength(0);
  });

  it("returns empty for assistant with no message", () => {
    const msgs = parseMessage("claude", { type: "assistant" });
    expect(msgs).toHaveLength(0);
  });

  it("maps multiple content blocks into multiple messages", () => {
    const msgs = parseMessage("claude", {
      type: "assistant",
      uuid: "multi",
      message: {
        content: [
          { type: "thinking", thinking: "hmm" },
          { type: "text", text: "answer" },
          { type: "tool_use", id: "t1", name: "Read", input: { path: "/a" } },
        ],
      },
    });
    expect(msgs).toHaveLength(3);
    expect(msgs.map((m) => m.type)).toEqual(["thinking", "text", "tool_call"]);
  });

  it("handles unknown type as 'unknown'", () => {
    const msg = firstOf(parseMessage("claude", { type: "weird_event" }));
    expect(msg.type).toBe("unknown");
  });

  it("preserves parent_tool_use_id as parentId", () => {
    const msgs = parseMessage("claude", {
      type: "assistant",
      uuid: "u1",
      parent_tool_use_id: "parent-tu",
      message: {
        content: [{ type: "text", text: "subagent response" }],
      },
    });
    expect(firstOf(msgs).parentId).toBe("parent-tu");
  });
});

// ── Codex adapter ───────────────────────────────────────────────────────────

describe("Codex adapter", () => {
  it("parses thread.started as init", () => {
    const msg = firstOf(
      parseMessage("codex", { type: "thread.started", thread_id: "th-1" })
    );
    expect(msg.type).toBe("init");
    expect(contentOf(msg).sessionId).toBe("th-1");
  });

  it("parses turn.started as status", () => {
    const msg = firstOf(
      parseMessage("codex", { type: "turn.started" })
    );
    expect(msg.type).toBe("status");
  });

  it("parses item.started with command_execution as tool_call", () => {
    const msgs = parseMessage("codex", {
      type: "item.started",
      item: {
        type: "command_execution",
        id: "cmd-1",
        command: "npm test",
      },
    });
    const msg = firstOf(msgs);
    expect(msg.type).toBe("tool_call");
    const c = contentOf(msg);
    expect(c.name).toBe("Bash");
    expect((c.input as Record<string, unknown>).command).toBe("npm test");
  });

  it("parses item.completed with command_execution as tool_result", () => {
    const msgs = parseMessage("codex", {
      type: "item.completed",
      item: {
        type: "command_execution",
        id: "cmd-1",
        aggregated_output: "All tests passed",
      },
    });
    const msg = firstOf(msgs);
    expect(msg.type).toBe("tool_result");
    expect(contentOf(msg).output).toBe("All tests passed");
  });

  it("parses item.completed with agent_message as text", () => {
    const msg = firstOf(
      parseMessage("codex", {
        type: "item.completed",
        item: { type: "agent_message", text: "Done!" },
      })
    );
    expect(msg.type).toBe("text");
    expect(contentOf(msg).text).toBe("Done!");
  });

  it("ignores item.started for agent_message (no output yet)", () => {
    const msgs = parseMessage("codex", {
      type: "item.started",
      item: { type: "agent_message", text: "" },
    });
    expect(msgs).toHaveLength(0);
  });

  it("parses turn.completed with usage as cost", () => {
    const msg = firstOf(
      parseMessage("codex", {
        type: "turn.completed",
        usage: { input_tokens: 200, output_tokens: 100 },
      })
    );
    expect(msg.type).toBe("cost");
    expect(contentOf(msg).inputTokens).toBe(200);
    expect(contentOf(msg).outputTokens).toBe(100);
  });

  it("parses turn.failed as error", () => {
    const msg = firstOf(
      parseMessage("codex", {
        type: "turn.failed",
        error: { message: "Rate limited" },
      })
    );
    expect(msg.type).toBe("error");
    expect(contentOf(msg).message).toBe("Rate limited");
  });

  it("parses error event", () => {
    const msg = firstOf(
      parseMessage("codex", { type: "error", message: "Connection lost" })
    );
    expect(msg.type).toBe("error");
    expect(contentOf(msg).message).toBe("Connection lost");
  });

  it("handles unknown codex type as 'unknown'", () => {
    const msg = firstOf(parseMessage("codex", { type: "weird" }));
    expect(msg.type).toBe("unknown");
  });
});

// ── Ollama adapter ──────────────────────────────────────────────────────────

describe("Ollama adapter", () => {
  it("parses system/init event", () => {
    const msg = firstOf(
      parseMessage("ollama", {
        type: "system",
        subtype: "init",
        session_id: "oll-1",
        model: "qwen3:8b",
        cwd: "/tmp",
      })
    );
    expect(msg.type).toBe("init");
    expect(contentOf(msg).model).toBe("qwen3:8b");
  });

  it("parses text chunk", () => {
    const msgs = parseMessage("ollama", {
      type: "chunk",
      message: { content: "Hello" },
      done: false,
    });
    const textMsgs = msgs.filter((m) => m.type === "text");
    expect(textMsgs).toHaveLength(1);
    expect(contentOf(textMsgs[0]).text).toBe("Hello");
  });

  it("parses thinking chunk", () => {
    const msgs = parseMessage("ollama", {
      type: "chunk",
      message: { thinking: "Let me think...", content: "" },
      done: false,
    });
    const thinkMsgs = msgs.filter((m) => m.type === "thinking");
    expect(thinkMsgs).toHaveLength(1);
    expect(contentOf(thinkMsgs[0]).text).toBe("Let me think...");
  });

  it("parses done chunk with cost", () => {
    const msgs = parseMessage("ollama", {
      type: "chunk",
      message: { content: "" },
      done: true,
      eval_duration: 5_000_000_000,
      prompt_eval_count: 100,
      eval_count: 50,
    });
    const costMsgs = msgs.filter((m) => m.type === "cost");
    expect(costMsgs).toHaveLength(1);
    const c = contentOf(costMsgs[0]);
    expect(c.totalCostUsd).toBe(0);
    expect(c.durationMs).toBe(5000);
    expect(c.inputTokens).toBe(100);
    expect(c.outputTokens).toBe(50);
  });

  it("parses error event", () => {
    const msg = firstOf(
      parseMessage("ollama", { type: "error", message: "Model not found" })
    );
    expect(msg.type).toBe("error");
    expect(contentOf(msg).message).toBe("Model not found");
  });

  it("handles unknown ollama type as 'unknown'", () => {
    const msg = firstOf(parseMessage("ollama", { type: "strange" }));
    expect(msg.type).toBe("unknown");
  });

  it("emits both thinking and text from same chunk", () => {
    const msgs = parseMessage("ollama", {
      type: "chunk",
      message: { thinking: "hmm", content: "answer" },
      done: false,
    });
    expect(msgs).toHaveLength(2);
    expect(msgs[0].type).toBe("thinking");
    expect(msgs[1].type).toBe("text");
  });
});

// ── Edge cases ──────────────────────────────────────────────────────────────

describe("Edge cases", () => {
  it("unknown provider falls back to claude adapter", () => {
    const msg = firstOf(
      parseMessage("unknown-provider" as ProviderId, {
        type: "system",
        subtype: "init",
        session_id: "s1",
        model: "test",
      })
    );
    expect(msg.type).toBe("init");
  });

  it("handles empty content gracefully", () => {
    const msgs = parseMessage("claude", {
      type: "assistant",
      message: { content: [] },
    });
    expect(msgs).toHaveLength(0);
  });

  it("all messages have id and timestamp", () => {
    const providers: ProviderId[] = ["claude", "codex", "ollama"];
    for (const p of providers) {
      const msgs = parseMessage(p, { type: "error", message: "test" });
      for (const m of msgs) {
        expect(typeof m.id).toBe("string");
        expect(m.id.length).toBeGreaterThan(0);
        expect(typeof m.timestamp).toBe("number");
        expect(m.timestamp).toBeGreaterThan(0);
      }
    }
  });

  it("str() guard returns fallback for non-string values", () => {
    // Pass a number where string is expected — should use fallback
    const msg = firstOf(
      parseMessage("claude", {
        type: "result",
        total_cost_usd: "not-a-number",
        usage: { input_tokens: "bad" },
      })
    );
    expect(msg.type).toBe("cost");
    // num() should return 0 for string values
    expect(contentOf(msg).totalCostUsd).toBe(0);
    expect(contentOf(msg).inputTokens).toBe(0);
  });
});
