import { describe, it, expect, vi, beforeEach } from "vitest";
import { extractModifiedFiles, trackFilesAndDetectConflicts } from "./ws-bridge-conflicts.js";
import type { Session } from "./ws-bridge-types.js";
import type { BrowserIncomingMessage, ContentBlock } from "./session-types.js";

// Mock session-names module
vi.mock("./session-names.js", () => ({
  getName: (id: string) => {
    const names: Record<string, string> = { s1: "Fix auth", s2: "Add tests", s3: "Refactor DB" };
    return names[id] || null;
  },
}));

function makeSession(id: string, modifiedFiles: string[] = [], hasCliSocket = true): Session {
  return {
    id,
    backendType: "claude",
    cliSocket: hasCliSocket ? ({} as any) : null,
    codexAdapter: null,
    browserSockets: new Set(),
    state: {
      session_id: id,
      model: "",
      cwd: "/project",
      tools: [],
      permissionMode: "default",
      claude_code_version: "",
      mcp_servers: [],
      agents: [],
      slash_commands: [],
      skills: [],
      total_cost_usd: 0,
      num_turns: 0,
      context_used_percent: 0,
      input_tokens: 0,
      output_tokens: 0,
      cache_read_tokens: 0,
      cache_creation_tokens: 0,
      context_window: 0,
      is_compacting: false,
      git_branch: "",
      is_worktree: false,
      is_containerized: false,
      repo_root: "",
      git_ahead: 0,
      git_behind: 0,
      total_lines_added: 0,
      total_lines_removed: 0,
    },
    pendingPermissions: new Map(),
    pendingControlRequests: new Map(),
    messageHistory: [],
    pendingMessages: [],
    nextEventSeq: 1,
    eventBuffer: [],
    lastAckSeq: 0,
    processedClientMessageIds: [],
    processedClientMessageIdSet: new Set(),
    messageQueue: [],
    queuePaused: false,
    keepAliveInterval: null,
    sentMessageBuffer: [],
    sentMessageSeq: 0,
    cliMessageSeqMap: new Map(),
    modifiedFiles: new Set(modifiedFiles),
  };
}

describe("extractModifiedFiles", () => {
  it("extracts file_path from Write tool_use blocks", () => {
    const blocks: ContentBlock[] = [
      { type: "text", text: "Writing file..." },
      { type: "tool_use", id: "tu1", name: "Write", input: { file_path: "/src/app.ts", content: "code" } },
    ];
    expect(extractModifiedFiles(blocks)).toEqual(["/src/app.ts"]);
  });

  it("extracts file_path from Edit tool_use blocks", () => {
    const blocks: ContentBlock[] = [
      { type: "tool_use", id: "tu1", name: "Edit", input: { file_path: "/src/utils.ts", old_string: "a", new_string: "b" } },
    ];
    expect(extractModifiedFiles(blocks)).toEqual(["/src/utils.ts"]);
  });

  it("extracts file_path from MultiEdit tool_use blocks", () => {
    const blocks: ContentBlock[] = [
      { type: "tool_use", id: "tu1", name: "MultiEdit", input: { file_path: "/config.json" } },
    ];
    expect(extractModifiedFiles(blocks)).toEqual(["/config.json"]);
  });

  it("extracts multiple files from multiple tool_use blocks", () => {
    const blocks: ContentBlock[] = [
      { type: "tool_use", id: "tu1", name: "Write", input: { file_path: "/a.ts" } },
      { type: "tool_use", id: "tu2", name: "Edit", input: { file_path: "/b.ts" } },
      { type: "text", text: "Done!" },
    ];
    expect(extractModifiedFiles(blocks)).toEqual(["/a.ts", "/b.ts"]);
  });

  it("ignores non-file-modifying tools", () => {
    const blocks: ContentBlock[] = [
      { type: "tool_use", id: "tu1", name: "Read", input: { file_path: "/readme.md" } },
      { type: "tool_use", id: "tu2", name: "Bash", input: { command: "ls" } },
    ];
    expect(extractModifiedFiles(blocks)).toEqual([]);
  });

  it("ignores tool_use blocks without file_path", () => {
    const blocks: ContentBlock[] = [
      { type: "tool_use", id: "tu1", name: "Write", input: {} },
    ];
    expect(extractModifiedFiles(blocks)).toEqual([]);
  });

  it("returns empty for blocks with no tool_use", () => {
    const blocks: ContentBlock[] = [
      { type: "text", text: "Hello" },
      { type: "thinking", thinking: "Thinking..." },
    ];
    expect(extractModifiedFiles(blocks)).toEqual([]);
  });
});

describe("trackFilesAndDetectConflicts", () => {
  let broadcastCalls: Array<{ session: Session; msg: BrowserIncomingMessage }>;
  let broadcastToBrowsers: (session: Session, msg: BrowserIncomingMessage) => void;

  beforeEach(() => {
    broadcastCalls = [];
    broadcastToBrowsers = (session, msg) => {
      broadcastCalls.push({ session, msg });
    };
  });

  it("adds files to the session's modifiedFiles set", () => {
    const session = makeSession("s1");
    trackFilesAndDetectConflicts(session, ["/a.ts", "/b.ts"], {
      getAllSessionEntries: () => [session],
      broadcastToBrowsers,
    });
    expect(session.modifiedFiles.has("/a.ts")).toBe(true);
    expect(session.modifiedFiles.has("/b.ts")).toBe(true);
  });

  it("does nothing when filePaths is empty", () => {
    const session = makeSession("s1");
    trackFilesAndDetectConflicts(session, [], {
      getAllSessionEntries: () => [session],
      broadcastToBrowsers,
    });
    expect(broadcastCalls).toHaveLength(0);
    expect(session.modifiedFiles.size).toBe(0);
  });

  it("detects conflict when another session has overlapping files", () => {
    const s1 = makeSession("s1");
    const s2 = makeSession("s2", ["/shared.ts"]);

    trackFilesAndDetectConflicts(s1, ["/shared.ts"], {
      getAllSessionEntries: () => [s1, s2],
      broadcastToBrowsers,
    });

    // Should broadcast to both sessions
    expect(broadcastCalls).toHaveLength(2);

    // Message to s1 about conflict with s2
    const toS1 = broadcastCalls.find((c) => c.session.id === "s1");
    expect(toS1).toBeDefined();
    expect(toS1!.msg).toMatchObject({
      type: "file_conflict",
      sessionId: "s1",
      conflictingSessionId: "s2",
      conflictingSessionName: "Add tests",
      files: ["/shared.ts"],
    });

    // Message to s2 about conflict with s1
    const toS2 = broadcastCalls.find((c) => c.session.id === "s2");
    expect(toS2).toBeDefined();
    expect(toS2!.msg).toMatchObject({
      type: "file_conflict",
      sessionId: "s2",
      conflictingSessionId: "s1",
      conflictingSessionName: "Fix auth",
      files: ["/shared.ts"],
    });
  });

  it("does not detect conflict when files don't overlap", () => {
    const s1 = makeSession("s1");
    const s2 = makeSession("s2", ["/other.ts"]);

    trackFilesAndDetectConflicts(s1, ["/unique.ts"], {
      getAllSessionEntries: () => [s1, s2],
      broadcastToBrowsers,
    });

    expect(broadcastCalls).toHaveLength(0);
  });

  it("skips sessions without active CLI connection", () => {
    const s1 = makeSession("s1");
    const s2 = makeSession("s2", ["/shared.ts"], false); // no CLI socket

    trackFilesAndDetectConflicts(s1, ["/shared.ts"], {
      getAllSessionEntries: () => [s1, s2],
      broadcastToBrowsers,
    });

    expect(broadcastCalls).toHaveLength(0);
  });

  it("detects conflicts with multiple sessions", () => {
    const s1 = makeSession("s1");
    const s2 = makeSession("s2", ["/shared.ts"]);
    const s3 = makeSession("s3", ["/shared.ts", "/another.ts"]);

    trackFilesAndDetectConflicts(s1, ["/shared.ts"], {
      getAllSessionEntries: () => [s1, s2, s3],
      broadcastToBrowsers,
    });

    // 2 conflicts: s1↔s2 and s1↔s3, each notifying both sides
    expect(broadcastCalls).toHaveLength(4);
  });

  it("uses fallback name when session has no stored name", () => {
    const s1 = makeSession("unknown-id-12345");
    const s2 = makeSession("s2", ["/file.ts"]);

    trackFilesAndDetectConflicts(s1, ["/file.ts"], {
      getAllSessionEntries: () => [s1, s2],
      broadcastToBrowsers,
    });

    const toS2 = broadcastCalls.find((c) => c.session.id === "s2");
    expect(toS2!.msg).toMatchObject({
      conflictingSessionName: "Session unknown-",
    });
  });
});
