// fileCommand: the GUI's file-control command line builder + structured
// dispatch. Mocks ../platform so importing this module never pulls in the
// real Tauri bridge (top-level await + native invoke) — same pattern as
// consoleDispatch.test.js.
import { beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../platform', () => ({ runCommandStructured: vi.fn() }));

import { buildCommandLine, dispatchFileCommand, quoteArg } from './fileCommand';

describe('quoteArg', () => {
  it('leaves a plain path unquoted', () => {
    expect(quoteArg('//Art/rig.blend')).toBe('//Art/rig.blend');
  });

  it('quotes a path containing a space', () => {
    expect(quoteArg('//a b/c.txt')).toBe('"//a b/c.txt"');
  });

  it('backslash-escapes an embedded double quote', () => {
    expect(quoteArg('a"b')).toBe('"a\\"b"');
  });

  it('backslash-escapes an embedded backslash', () => {
    expect(quoteArg('a\\b')).toBe('"a\\\\b"');
  });

  it('escapes backslash and quote together in the order shlex round-trips', () => {
    // Content: a \ " b (4 real chars) — backslash escaped first, then the
    // quote, so parse_line's shlex sees \\ then \" and recovers exactly that.
    expect(quoteArg('a\\"b')).toBe('"a\\\\\\"b"');
  });

  it('quotes an empty string so it is not lost entirely', () => {
    expect(quoteArg('')).toBe('""');
  });

  it('quotes a path containing an apostrophe, leaving it unescaped', () => {
    expect(quoteArg("o'brien.txt")).toBe('"o\'brien.txt"');
  });
});

describe('buildCommandLine', () => {
  it('quotes a path with a space, prefixing // (the brief example)', () => {
    expect(buildCommandLine('checkout', ['a b/c.txt'])).toBe('checkout "//a b/c.txt"');
  });

  it('leaves an already-depot path alone', () => {
    expect(buildCommandLine('checkout', ['//Art/rig.blend'])).toBe('checkout //Art/rig.blend');
  });

  it('joins multiple paths in order', () => {
    expect(buildCommandLine('add', ['a.txt', 'b.txt'])).toBe('add //a.txt //b.txt');
  });

  it('appends -f when force is set', () => {
    expect(buildCommandLine('revert', ['a.txt'], { force: true })).toBe('revert //a.txt -f');
  });

  it('omits -f when force is falsy', () => {
    expect(buildCommandLine('revert', ['a.txt'])).toBe('revert //a.txt');
  });

  it('appends -m <message>, quoted', () => {
    expect(buildCommandLine('submit', [], { message: 'first pass at the rig' })).toBe(
      'submit -m "first pass at the rig"'
    );
  });

  it('leaves an unquotable message bare', () => {
    expect(buildCommandLine('submit', [], { message: 'fix' })).toBe('submit -m fix');
  });

  it('handles no paths and no options', () => {
    expect(buildCommandLine('pull', [])).toBe('pull');
  });

  it('quotes a path containing an apostrophe and prefixes //', () => {
    expect(buildCommandLine('checkout', ["data/o'brien.txt"])).toBe('checkout "//data/o\'brien.txt"');
  });

  it('quotes a message containing an apostrophe, leaving it unescaped', () => {
    expect(buildCommandLine('submit', [], { message: "it's done" })).toBe('submit -m "it\'s done"');
  });
});

describe('dispatchFileCommand', () => {
  const conn = {
    api: { baseUrl: 'https://depot.example:8666', token: 'tok-abc' },
    account: { username: '@robin', deviceName: "Robin's Laptop" },
    deviceId: 'device-123',
    activeProjectId: 'proj-1',
    projectName: 'alpha',
    workspacePath: '/home/robin/ws',
  };

  const makeRefreshers = () => ({
    refreshDepotTree: vi.fn(),
    refreshRevisions: vi.fn(),
    refreshLocks: vi.fn(),
    refreshUsers: vi.fn(),
    refreshProjects: vi.fn(),
    reloadWorkspace: vi.fn(),
  });

  beforeEach(() => vi.clearAllMocks());

  it('runs the line with the built ctx and fans out files+locks, including reloadWorkspace', async () => {
    const result = { ok: true, code: 0, human: 'Checked out //a.txt.', data: null, changed: ['files', 'locks'] };
    const runner = vi.fn().mockResolvedValue(result);
    const r = makeRefreshers();

    const returned = await dispatchFileCommand('checkout //a.txt', conn, r, { runner });

    expect(runner).toHaveBeenCalledTimes(1);
    const [line, ctx, onProgress] = runner.mock.calls[0];
    expect(line).toBe('checkout //a.txt');
    expect(ctx.username).toBe('@robin');
    expect(onProgress).toBeUndefined();
    expect(returned).toBe(result);

    expect(r.refreshDepotTree).toHaveBeenCalledTimes(1);
    expect(r.refreshRevisions).toHaveBeenCalledTimes(1);
    expect(r.refreshLocks).toHaveBeenCalledTimes(1);
    expect(r.reloadWorkspace).toHaveBeenCalledTimes(1);
    expect(r.refreshUsers).not.toHaveBeenCalled();
  });

  it('does not fire any refresher when the command failed', async () => {
    const runner = vi.fn().mockResolvedValue({ ok: false, code: 1, human: 'boom', data: null, changed: ['files', 'locks'] });
    const r = makeRefreshers();

    await dispatchFileCommand('checkout //a.txt', conn, r, { runner });

    for (const fn of Object.values(r)) expect(fn).not.toHaveBeenCalled();
  });

  it('skips reloadWorkspace when changed does not include files', async () => {
    const runner = vi.fn().mockResolvedValue({ ok: true, code: 0, human: 'Force-released.', data: null, changed: ['locks'] });
    const r = makeRefreshers();

    await dispatchFileCommand('unlock -f //a.txt', conn, r, { runner });

    expect(r.refreshLocks).toHaveBeenCalledTimes(1);
    expect(r.reloadWorkspace).not.toHaveBeenCalled();
  });

  it('passes onProgress through to the runner', async () => {
    const runner = vi.fn().mockResolvedValue({ ok: true, code: 0, human: 'ok', data: null, changed: [] });
    const onProgress = vi.fn();

    await dispatchFileCommand('submit -m x', conn, makeRefreshers(), { runner, onProgress });

    const [, , passedProgress] = runner.mock.calls[0];
    expect(passedProgress).toBe(onProgress);
  });
});
