// Console dispatch: the ctx handed to Rust's run_console_command, and the
// changed→refresh fan-out. Mocks ../platform so importing this module never
// pulls in the real Tauri bridge (top-level await + native invoke).
import { beforeEach, describe, expect, it, vi } from 'vitest';

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

import { applyConsoleChanged, buildConsoleCtx, dispatchConsole } from './consoleDispatch';

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',
};

describe('buildConsoleCtx', () => {
  it('maps the connection state into the camelCase ConsoleCtx Rust expects', () => {
    const ctx = buildConsoleCtx(conn);
    expect(ctx).toEqual({
      server: 'https://depot.example:8666',
      token: 'tok-abc',
      username: '@robin',
      deviceId: 'device-123',
      deviceName: "Robin's Laptop",
      projectId: 'proj-1',
      projectName: 'alpha',
      workspacePath: '/home/robin/ws',
    });
  });

  it('falls back to nulls when no project/workspace is selected', () => {
    const ctx = buildConsoleCtx({
      api: { baseUrl: 'http://127.0.0.1:8788', token: 't' },
      account: { username: '@owner', deviceName: 'box' },
      deviceId: 'd',
      activeProjectId: null,
      projectName: null,
      workspacePath: '',
    });
    expect(ctx.projectId).toBeNull();
    expect(ctx.projectName).toBeNull();
    expect(ctx.workspacePath).toBeNull();
  });
});

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

  it('locks hint triggers only the locks refresher', () => {
    const r = makeRefreshers();
    applyConsoleChanged(['locks'], r);
    expect(r.refreshLocks).toHaveBeenCalledTimes(1);
    expect(r.refreshDepotTree).not.toHaveBeenCalled();
    expect(r.refreshUsers).not.toHaveBeenCalled();
  });

  it('files hint refreshes both the depot tree and revisions', () => {
    const r = makeRefreshers();
    applyConsoleChanged(['files'], r);
    expect(r.refreshDepotTree).toHaveBeenCalledTimes(1);
    expect(r.refreshRevisions).toHaveBeenCalledTimes(1);
  });

  it('ignores unknown hints and a missing changed list', () => {
    const r = makeRefreshers();
    applyConsoleChanged(['nonsense'], r);
    applyConsoleChanged(undefined, r);
    for (const fn of Object.values(r)) expect(fn).not.toHaveBeenCalled();
  });
});

describe('dispatchConsole', () => {
  beforeEach(() => vi.clearAllMocks());

  it('runs the line with the built ctx and fans a changed:[locks] result out', async () => {
    const runner = vi.fn().mockResolvedValue({ ok: true, output: 'Force-released: //a', changed: ['locks'] });
    const r = {
      refreshDepotTree: vi.fn(),
      refreshRevisions: vi.fn(),
      refreshLocks: vi.fn(),
      refreshUsers: vi.fn(),
      refreshProjects: vi.fn(),
    };
    const { ctx, result } = await dispatchConsole('unlock -f //a', conn, r, runner);

    expect(runner).toHaveBeenCalledTimes(1);
    const [line, passedCtx] = runner.mock.calls[0];
    expect(line).toBe('unlock -f //a');
    expect(passedCtx.username).toBe('@robin');
    expect(passedCtx.deviceId).toBe('device-123');
    expect(ctx).toBe(passedCtx);
    expect(result.ok).toBe(true);
    expect(r.refreshLocks).toHaveBeenCalledTimes(1);
  });

  it('does not fire refreshers when the command failed', async () => {
    const runner = vi.fn().mockResolvedValue({ ok: false, output: 'boom', changed: ['locks'] });
    const r = { refreshLocks: vi.fn() };
    await dispatchConsole('unlock //a', conn, r, runner);
    expect(r.refreshLocks).not.toHaveBeenCalled();
  });
});
