import { describe, it, expect, vi } from 'vitest';
import type { ToolContext } from './core.js';
import { executeTool } from './package-request.js';

const ctx = (over: Partial<ToolContext> = {}): ToolContext =>
  ({ workspacePath: '/ws', ...over } as ToolContext);

describe('RequestPackage tool', () => {
  it('ignores non-RequestPackage names (returns null)', async () => {
    expect(await executeTool('Bash', { name: 'x', reason: 'y' }, ctx())).toBeNull();
  });

  it('requires name and reason', async () => {
    expect((await executeTool('RequestPackage', { reason: 'r' }, ctx()))?.isError).toBe(true);
    expect((await executeTool('RequestPackage', { name: 'requests' }, ctx()))?.isError).toBe(true);
  });

  it('rejects an invalid / shadowing spec with a hard error (records nothing)', async () => {
    const rec = vi.fn();
    // "os" shadows the stdlib → assertNotShadowing throws.
    const r = await executeTool('RequestPackage', { name: 'os', reason: 'need it' }, ctx({ recordPackageRequest: rec, toolApprovalInteractive: true }));
    expect(r?.isError).toBe(true);
    expect(rec).not.toHaveBeenCalled();
    // a range operator is also rejected.
    const r2 = await executeTool('RequestPackage', { name: 'requests>=2', reason: 'x' }, ctx({ recordPackageRequest: rec, toolApprovalInteractive: true }));
    expect(r2?.isError).toBe(true);
  });

  it('reports feature unavailable when no recorder is bound (records nothing)', async () => {
    const r = await executeTool('RequestPackage', { name: 'requests', reason: 'http' }, ctx());
    expect(r?.isError).toBe(false);
    expect(String(r?.output)).toContain('利用できません');
  });

  it('short-circuits an already-installed package (exact spec match)', async () => {
    const rec = vi.fn();
    const c = ctx({ recordPackageRequest: rec, installedPackages: [{ name: 'requests', spec: 'requests==2.32.3' }], toolApprovalInteractive: true });
    const r = await executeTool('RequestPackage', { name: 'requests==2.32.3', reason: 'http' }, c);
    expect(r?.isError).toBe(false);
    expect(String(r?.output)).toContain('インストール済み');
    expect(rec).not.toHaveBeenCalled();
    expect(c.pendingPackageApproval).toBeUndefined(); // no park
  });

  it('short-circuits a bare-name request when any version is installed', async () => {
    const rec = vi.fn();
    const c = ctx({ recordPackageRequest: rec, installedPackages: [{ name: 'requests', spec: 'requests==2.32.3' }], toolApprovalInteractive: true });
    const r = await executeTool('RequestPackage', { name: 'requests', reason: 'http' }, c);
    expect(String(r?.output)).toContain('インストール済み');
    expect(c.pendingPackageApproval).toBeUndefined();
  });

  it('does NOT park a pinned request for a different version than the one installed', async () => {
    const rec = vi.fn();
    const c = ctx({ recordPackageRequest: rec, installedPackages: [{ name: 'httpx', spec: 'httpx==0.27.0' }], toolApprovalInteractive: true });
    const r = await executeTool('RequestPackage', { name: 'httpx==0.28.0', reason: 'need newer' }, c);
    expect(r?.isError).toBe(false);
    expect(String(r?.output)).toContain('固定');
    expect(rec).not.toHaveBeenCalled();
    expect(c.pendingPackageApproval).toBeUndefined(); // no dead-end park
  });

  it('does NOT re-park a package already declined for this job (deny loop guard)', async () => {
    const rec = vi.fn();
    const c = ctx({ recordPackageRequest: rec, declinedPackages: ['httpx'], toolApprovalInteractive: true });
    const r = await executeTool('RequestPackage', { name: 'httpx==0.28.0', reason: 'retry' }, c);
    expect(r?.isError).toBe(false);
    expect(String(r?.output)).toContain('却下');
    expect(rec).not.toHaveBeenCalled();
    expect(c.pendingPackageApproval).toBeUndefined();
  });

  it('parks for approval on an interactive run (records pending)', async () => {
    const rec = vi.fn();
    const c = ctx({ recordPackageRequest: rec, toolApprovalInteractive: true });
    const r = await executeTool('RequestPackage', { name: 'Requests==2.32.3', reason: 'http calls' }, c);
    expect(r?.isError).toBe(false);
    expect(c.pendingPackageApproval).toEqual({ spec: 'requests==2.32.3' });
    expect(rec).toHaveBeenCalledWith(
      expect.objectContaining({ spec: 'requests==2.32.3', normalizedName: 'requests', status: 'pending', reason: 'http calls' }),
    );
  });

  it('records auto_denied and does NOT park on a non-interactive run', async () => {
    const rec = vi.fn();
    const c = ctx({ recordPackageRequest: rec, toolApprovalInteractive: false });
    // httpx is neither stdlib nor preinstalled, so it passes the shadowing guard.
    const r = await executeTool('RequestPackage', { name: 'httpx', reason: 'async http' }, c);
    expect(r?.isError).toBe(false);
    expect(c.pendingPackageApproval).toBeUndefined();
    expect(rec).toHaveBeenCalledWith(expect.objectContaining({ normalizedName: 'httpx', status: 'auto_denied' }));
  });
});
