import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createCli } from './index.js';

const mockClient = {
  errand: {
    create: { mutate: vi.fn() },
    list: { query: vi.fn() },
    get: { query: vi.fn() },
    diff: { query: vi.fn() },
    complete: { mutate: vi.fn() },
    merge: { mutate: vi.fn() },
    delete: { mutate: vi.fn() },
    sendMessage: { mutate: vi.fn() },
    abandon: { mutate: vi.fn() },
  },
};

vi.mock('./trpc-client.js', () => ({
  createDefaultTrpcClient: () => mockClient,
}));

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

async function runCli(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
  const stdoutLines: string[] = [];
  const stderrLines: string[] = [];
  let exitCode = 0;

  vi.spyOn(process.stdout, 'write').mockImplementation((s: any) => { stdoutLines.push(String(s)); return true; });
  vi.spyOn(process.stderr, 'write').mockImplementation((s: any) => { stderrLines.push(String(s)); return true; });
  vi.spyOn(console, 'log').mockImplementation((...a: any[]) => { stdoutLines.push(a.join(' ')); });
  vi.spyOn(console, 'error').mockImplementation((...a: any[]) => { stderrLines.push(a.join(' ')); });
  vi.spyOn(process, 'exit').mockImplementation((code?: any) => { exitCode = code ?? 0; throw new Error(`process.exit(${code})`); });

  const program = createCli();
  try {
    await program.parseAsync(['node', 'cw', ...args]);
  } catch (e: any) {
    if (!e.message?.startsWith('process.exit')) throw e;
  }

  vi.restoreAllMocks();
  return {
    stdout: stdoutLines.join('\n'),
    stderr: stderrLines.join('\n'),
    exitCode,
  };
}

describe('cw errand start', () => {
  it('calls errand.create.mutate with correct args and prints output', async () => {
    mockClient.errand.create.mutate.mockResolvedValueOnce({
      id: 'errand-abc123',
      branch: 'cw/errand/fix-typo-errand-ab',
      agentId: 'agent-xyz',
    });
    const { stdout, exitCode } = await runCli(['errand', 'start', 'fix typo', '--project', 'proj-1']);
    expect(mockClient.errand.create.mutate).toHaveBeenCalledWith({
      description: 'fix typo',
      projectId: 'proj-1',
      baseBranch: undefined,
    });
    expect(stdout).toContain('Errand started');
    expect(stdout).toContain('errand-abc123');
    expect(exitCode).toBe(0);
  });

  it('exits 1 and prints length error without calling tRPC when description > 200 chars', async () => {
    const longDesc = 'x'.repeat(201);
    const { stderr, exitCode } = await runCli(['errand', 'start', longDesc, '--project', 'proj-1']);
    expect(mockClient.errand.create.mutate).not.toHaveBeenCalled();
    expect(stderr).toContain('description must be ≤200 characters (201 given)');
    expect(exitCode).toBe(1);
  });

  it('passes --base option as baseBranch', async () => {
    mockClient.errand.create.mutate.mockResolvedValueOnce({ id: 'e1', branch: 'b', agentId: 'a' });
    await runCli(['errand', 'start', 'fix thing', '--project', 'p1', '--base', 'develop']);
    expect(mockClient.errand.create.mutate).toHaveBeenCalledWith(
      expect.objectContaining({ baseBranch: 'develop' })
    );
  });
});

describe('cw errand list', () => {
  it('prints tab-separated rows for errands', async () => {
    mockClient.errand.list.query.mockResolvedValueOnce([
      { id: 'errand-abc123full', description: 'fix the bug', branch: 'cw/errand/fix-bug-errand-ab', status: 'active', agentAlias: 'my-agent' },
    ]);
    const { stdout } = await runCli(['errand', 'list']);
    expect(stdout).toContain('errand-a'); // id.slice(0,8)
    expect(stdout).toContain('fix the bug');
    expect(stdout).toContain('active');
    expect(stdout).toContain('my-agent');
  });

  it('prints "No errands found" on empty result', async () => {
    mockClient.errand.list.query.mockResolvedValueOnce([]);
    const { stdout } = await runCli(['errand', 'list']);
    expect(stdout).toContain('No errands found');
  });

  it('truncates description at 60 chars with ellipsis', async () => {
    const longDesc = 'a'.repeat(65);
    mockClient.errand.list.query.mockResolvedValueOnce([
      { id: 'x'.repeat(16), description: longDesc, branch: 'b', status: 'active', agentAlias: null },
    ]);
    const { stdout } = await runCli(['errand', 'list']);
    expect(stdout).toContain('a'.repeat(57) + '...');
  });

  it('passes --status filter to query', async () => {
    mockClient.errand.list.query.mockResolvedValueOnce([]);
    await runCli(['errand', 'list', '--status', 'active']);
    expect(mockClient.errand.list.query).toHaveBeenCalledWith(expect.objectContaining({ status: 'active' }));
  });

  it('passes --project filter to query', async () => {
    mockClient.errand.list.query.mockResolvedValueOnce([]);
    await runCli(['errand', 'list', '--project', 'proj-99']);
    expect(mockClient.errand.list.query).toHaveBeenCalledWith(expect.objectContaining({ projectId: 'proj-99' }));
  });

  it('shows "-" for null agentAlias', async () => {
    mockClient.errand.list.query.mockResolvedValueOnce([
      { id: 'x'.repeat(16), description: 'test', branch: 'b', status: 'active', agentAlias: null },
    ]);
    const { stdout } = await runCli(['errand', 'list']);
    expect(stdout).toContain('-');
  });
});

describe('cw errand chat', () => {
  it('calls sendMessage.mutate with no stdout on success', async () => {
    mockClient.errand.sendMessage.mutate.mockResolvedValueOnce({ success: true });
    const { stdout, exitCode } = await runCli(['errand', 'chat', 'e1', 'hello there']);
    expect(mockClient.errand.sendMessage.mutate).toHaveBeenCalledWith({ id: 'e1', message: 'hello there' });
    expect(stdout.trim()).toBe('');
    expect(exitCode).toBe(0);
  });

  it('exits 1 and prints error when tRPC throws (agent not running)', async () => {
    mockClient.errand.sendMessage.mutate.mockRejectedValueOnce(new Error('Agent is not running (status: stopped)'));
    const { stderr, exitCode } = await runCli(['errand', 'chat', 'e1', 'msg']);
    expect(stderr).toContain('Agent is not running');
    expect(exitCode).toBe(1);
  });
});

describe('cw errand diff', () => {
  it('writes raw diff to stdout and exits 0', async () => {
    mockClient.errand.diff.query.mockResolvedValueOnce({ diff: 'diff --git a/foo.ts b/foo.ts\n+++ change' });
    const { stdout, exitCode } = await runCli(['errand', 'diff', 'e1']);
    expect(stdout).toContain('diff --git');
    expect(exitCode).toBe(0);
  });

  it('produces no output on empty diff and exits 0', async () => {
    mockClient.errand.diff.query.mockResolvedValueOnce({ diff: '' });
    const { stdout, exitCode } = await runCli(['errand', 'diff', 'e1']);
    expect(stdout.trim()).toBe('');
    expect(exitCode).toBe(0);
  });

  it('exits 1 with "Errand <id> not found" on NOT_FOUND error', async () => {
    mockClient.errand.diff.query.mockRejectedValueOnce(new Error('NOT_FOUND: errand not found'));
    const { stderr, exitCode } = await runCli(['errand', 'diff', 'missing-id']);
    expect(stderr).toContain('Errand missing-id not found');
    expect(exitCode).toBe(1);
  });
});

describe('cw errand complete', () => {
  it('prints "Errand <id> marked as ready for review"', async () => {
    mockClient.errand.complete.mutate.mockResolvedValueOnce({});
    const { stdout, exitCode } = await runCli(['errand', 'complete', 'errand-1']);
    expect(stdout).toContain('Errand errand-1 marked as ready for review');
    expect(exitCode).toBe(0);
  });
});

describe('cw errand merge', () => {
  it('prints "Merged <branch> into <baseBranch>" on clean merge', async () => {
    mockClient.errand.get.query.mockResolvedValueOnce({
      id: 'e1', branch: 'cw/errand/fix-bug-e1', baseBranch: 'main', status: 'pending_review',
      conflictFiles: [], projectPath: '/path/to/repo',
    });
    mockClient.errand.merge.mutate.mockResolvedValueOnce({ status: 'merged' });
    const { stdout, exitCode } = await runCli(['errand', 'merge', 'e1']);
    expect(stdout).toContain('Merged cw/errand/fix-bug-e1 into main');
    expect(exitCode).toBe(0);
  });

  it('exits 1 and prints conflicting files on conflict', async () => {
    mockClient.errand.get.query.mockResolvedValueOnce({
      id: 'e1', branch: 'cw/errand/fix-bug-e1', baseBranch: 'main', status: 'pending_review',
      conflictFiles: [], projectPath: '/repo',
    });
    const conflictError = Object.assign(new Error('Merge conflict'), {
      data: { conflictFiles: ['src/a.ts', 'src/b.ts'] },
    });
    mockClient.errand.merge.mutate.mockRejectedValueOnce(conflictError);
    const { stderr, exitCode } = await runCli(['errand', 'merge', 'e1']);
    expect(stderr).toContain('Merge conflict in 2 file(s)');
    expect(stderr).toContain('src/a.ts');
    expect(stderr).toContain('src/b.ts');
    expect(stderr).toContain('Run: cw errand resolve e1');
    expect(exitCode).toBe(1);
  });

  it('uses --target override instead of baseBranch', async () => {
    mockClient.errand.get.query.mockResolvedValueOnce({
      id: 'e1', branch: 'cw/errand/fix-e1', baseBranch: 'main', status: 'pending_review',
      conflictFiles: [], projectPath: '/repo',
    });
    mockClient.errand.merge.mutate.mockResolvedValueOnce({ status: 'merged' });
    const { stdout } = await runCli(['errand', 'merge', 'e1', '--target', 'develop']);
    expect(stdout).toContain('Merged cw/errand/fix-e1 into develop');
    expect(mockClient.errand.merge.mutate).toHaveBeenCalledWith({ id: 'e1', target: 'develop' });
  });
});

describe('cw errand resolve', () => {
  it('prints worktree path and conflicting files when status is conflict', async () => {
    mockClient.errand.get.query.mockResolvedValueOnce({
      id: 'e1', status: 'conflict', conflictFiles: ['src/a.ts', 'src/b.ts'],
      projectPath: '/home/user/project', branch: 'cw/errand/fix-e1', baseBranch: 'main',
    });
    const { stdout, exitCode } = await runCli(['errand', 'resolve', 'e1']);
    expect(stdout).toContain('/home/user/project/.cw-worktrees/e1');
    expect(stdout).toContain('src/a.ts');
    expect(stdout).toContain('src/b.ts');
    expect(stdout).toContain('cw errand merge e1');
    expect(exitCode).toBe(0);
  });

  it('exits 1 with status message when errand is not in conflict', async () => {
    mockClient.errand.get.query.mockResolvedValueOnce({
      id: 'e1', status: 'pending_review', conflictFiles: [], projectPath: '/repo',
    });
    const { stderr, exitCode } = await runCli(['errand', 'resolve', 'e1']);
    expect(stderr).toContain('is not in conflict');
    expect(stderr).toContain('pending_review');
    expect(exitCode).toBe(1);
  });
});

describe('cw errand abandon', () => {
  it('prints "Errand <id> abandoned"', async () => {
    mockClient.errand.abandon.mutate.mockResolvedValueOnce({});
    const { stdout, exitCode } = await runCli(['errand', 'abandon', 'errand-1']);
    expect(stdout).toContain('Errand errand-1 abandoned');
    expect(exitCode).toBe(0);
  });
});

describe('cw errand delete', () => {
  it('prints "Errand <id> deleted"', async () => {
    mockClient.errand.delete.mutate.mockResolvedValueOnce({ success: true });
    const { stdout, exitCode } = await runCli(['errand', 'delete', 'errand-1']);
    expect(stdout).toContain('Errand errand-1 deleted');
    expect(exitCode).toBe(0);
  });
});
