import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { CommandsPanel } from './commands-panel';

beforeAll(() => {
  Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
    configurable: true,
    value: true,
  });
});

afterAll(() => {
  Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
});

let root: Root | null;
let container: HTMLElement | null;

async function render(node: Parameters<Root['render']>[0]): Promise<void> {
  container = document.createElement('div');
  document.body.append(container);
  root = createRoot(container);
  await act(async () => {
    root?.render(node);
  });
}

afterEach(async () => {
  await act(async () => {
    root?.unmount();
  });
  document.body.replaceChildren();
  root = null;
  container = null;
});

describe('CommandsPanel', () => {
  it('shows the frozen local pendingApproval args in the confirmation area, regardless of misleading server message text', async () => {
    await render(
      <CommandsPanel
        manifest={null}
        results={[]}
        approval={{
          conversationId: 'c1',
          command: 'deploy', // matches pendingApproval — this is a legitimately approved request
          success: true,
          approvalId: 'ap1',
          expiresAt: '2026-01-01T00:00:00.000Z',
          // Free-text server message claims a different, less alarming target
          // than what will actually be sent — the UI must not rely on this.
          message: 'This will only affect the staging environment.',
        }}
        pendingApproval={{ command: 'deploy', args: 'prod' }}
        hasConversation
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    // The exact frozen combined action is visible...
    expect(container?.textContent).toContain('/deploy');
    expect(container?.textContent).toContain('prod');
    // ...and the misleading server free-text is never shown next to it.
    expect(container?.textContent).not.toContain('staging environment');
  });

  it('does not throw when a manifest commands entry is null', async () => {
    const manifest = {
      commands: [
        null,
        {
          name: 'model',
          aliases: [],
          description: 'Change the active model',
          scope: 'core',
          execution: 'socket',
          available: true,
        },
      ],
      skills: [null],
      version: 1,
    } as unknown as Parameters<typeof CommandsPanel>[0]['manifest'];

    await expect(
      render(
        <CommandsPanel
          manifest={manifest}
          results={[]}
          approval={null}
          pendingApproval={null}
          hasConversation={false}
          onExecute={vi.fn()}
          onApprove={vi.fn()}
          onRunApproved={vi.fn()}
        />,
      ),
    ).resolves.not.toThrow();

    expect(container?.textContent).toContain('model');
  });

  it('shows an explicit no-args fallback when the frozen pendingApproval has no args', async () => {
    await render(
      <CommandsPanel
        manifest={null}
        results={[]}
        approval={{
          conversationId: 'c1',
          command: 'deploy',
          success: true,
          approvalId: 'ap1',
          expiresAt: '2026-01-01T00:00:00.000Z',
        }}
        pendingApproval={{ command: 'deploy' }}
        hasConversation
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    expect(container?.textContent?.toLowerCase()).toContain('no args');
  });

  it('renders skills from a skills-only manifest', async () => {
    await render(
      <CommandsPanel
        manifest={{
          commands: [],
          skills: [{ name: 'brave-search', description: 'Search the web', available: true }],
          version: 1,
        }}
        results={[]}
        approval={null}
        pendingApproval={null}
        hasConversation={false}
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    expect(container?.textContent).toContain('brave-search');
    expect(container?.textContent).toContain('Search the web');
  });

  it('does not show the Run affordance when approval.success/approvalId are objects, even though command matches pendingApproval', async () => {
    const approval = {
      conversationId: 'c1',
      command: 'deploy',
      success: { truthy: 'object' },
      approvalId: { also: 'object' },
    } as unknown as Parameters<typeof CommandsPanel>[0]['approval'];

    await render(
      <CommandsPanel
        manifest={null}
        results={[]}
        approval={approval}
        pendingApproval={{ command: 'deploy', args: 'prod' }}
        hasConversation
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    expect(
      [...(container?.querySelectorAll('button') ?? [])].some((button) =>
        button.textContent?.includes('Run approved command'),
      ),
    ).toBe(false);
  });

  it('shows the guarded server-provided denial reason for a denied approval', async () => {
    await render(
      <CommandsPanel
        manifest={null}
        results={[]}
        approval={{
          conversationId: 'c1',
          command: 'deploy',
          success: false,
          message: 'Not authorized',
        }}
        pendingApproval={{ command: 'deploy', args: 'prod' }}
        hasConversation
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    expect(container?.textContent).toContain('Not authorized');
  });

  it('falls back to a stable "Denied." copy when a denial has no usable message', async () => {
    await render(
      <CommandsPanel
        manifest={null}
        results={[]}
        approval={{ conversationId: 'c1', command: 'deploy', success: false }}
        pendingApproval={{ command: 'deploy', args: 'prod' }}
        hasConversation
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    expect(container?.textContent).toContain('Denied.');
  });

  it('shows the guarded contract-provided reason for a failed command result, falling back to a stable copy only when absent', async () => {
    await render(
      <CommandsPanel
        manifest={null}
        results={[
          { conversationId: 'c1', command: 'model', success: false, message: 'Unknown model' },
          { conversationId: 'c1', command: 'deploy', success: false },
        ]}
        approval={null}
        pendingApproval={null}
        hasConversation={false}
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    expect(container?.textContent).toContain('Unknown model');
    expect(container?.textContent).toContain('Command failed.');
  });

  it('bounds an oversized command result message at the render site as defense-in-depth', async () => {
    const hostileMessage = 'y'.repeat(50_000);
    await render(
      <CommandsPanel
        manifest={null}
        results={[
          { conversationId: 'c1', command: 'model', success: false, message: hostileMessage },
        ]}
        approval={null}
        pendingApproval={null}
        hasConversation={false}
        onExecute={vi.fn()}
        onApprove={vi.fn()}
        onRunApproved={vi.fn()}
      />,
    );

    const text = container?.textContent ?? '';
    expect(text.length).toBeLessThan(hostileMessage.length);
  });

  it('does not throw when the manifest fields are malformed (non-array commands/skills)', async () => {
    const manifest = {
      commands: 'not-an-array',
      skills: null,
      version: 1,
    } as unknown as Parameters<typeof CommandsPanel>[0]['manifest'];

    await expect(
      render(
        <CommandsPanel
          manifest={manifest}
          results={[]}
          approval={null}
          pendingApproval={null}
          hasConversation={false}
          onExecute={vi.fn()}
          onApprove={vi.fn()}
          onRunApproved={vi.fn()}
        />,
      ),
    ).resolves.not.toThrow();
  });
});
