import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
import type { CliInstallationStatus } from '@shared/types';
import type { SkillCatalogItem } from '@shared/types/extensions';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';

interface StoreState {
  fetchSkillsCatalog: ReturnType<typeof vi.fn>;
  fetchSkillDetail: ReturnType<typeof vi.fn>;
  skillsCatalogLoadingByProjectPath: Record<string, boolean>;
  skillsCatalogErrorByProjectPath: Record<string, string | null>;
  skillsDetailsById: Record<string, unknown>;
  skillsUserCatalog: SkillCatalogItem[];
  skillsProjectCatalogByProjectPath: Record<string, SkillCatalogItem[]>;
  cliStatus: CliInstallationStatus | null;
  cliStatusLoading: boolean;
  appConfig: {
    general: {
      multimodelEnabled: boolean;
    };
  } | null;
}

const storeState = {} as StoreState;
const startWatchingMock = vi.fn();
const stopWatchingMock = vi.fn();
const onChangedMock = vi.fn();
const codexAccountHookState = {
  snapshot: null as CodexAccountSnapshotDto | null,
  loading: false,
  error: null as string | null,
  refresh: vi.fn(() => Promise.resolve(undefined)),
  startChatgptLogin: vi.fn(() => Promise.resolve(true)),
  cancelChatgptLogin: vi.fn(() => Promise.resolve(true)),
  logout: vi.fn(() => Promise.resolve(true)),
};
let skillsChangedHandler: ((event: {
  scope: 'user' | 'project';
  projectPath: string | null;
  path: string;
  type: 'create' | 'change' | 'delete';
}) => void) | null = null;

vi.mock('@renderer/store', () => ({
  useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
}));

vi.mock('@features/codex-account/renderer', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@features/codex-account/renderer')>();
  return {
    ...actual,
    useCodexAccountSnapshot: () => codexAccountHookState,
  };
});

vi.mock('zustand/react/shallow', () => ({
  useShallow: <T,>(selector: T) => selector,
}));

vi.mock('@renderer/api', () => ({
  api: {
    skills: {
      startWatching: (...args: unknown[]) => startWatchingMock(...args),
      stopWatching: (...args: unknown[]) => stopWatchingMock(...args),
      onChanged: (...args: unknown[]) => onChangedMock(...args),
    },
  },
}));

vi.mock('@renderer/components/ui/badge', () => ({
  Badge: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
}));

vi.mock('@renderer/components/ui/button', () => ({
  Button: ({
    children,
    onClick,
    type = 'button',
  }: React.PropsWithChildren<{
    onClick?: () => void;
    type?: 'button' | 'submit' | 'reset';
    variant?: string;
    size?: string;
    className?: string;
  }>) =>
    React.createElement(
      'button',
      {
        type,
        onClick,
      },
      children
    ),
}));

vi.mock('@renderer/components/ui/popover', () => ({
  Popover: ({ children }: React.PropsWithChildren<{ open?: boolean; onOpenChange?: (open: boolean) => void }>) =>
    React.createElement(React.Fragment, null, children),
  PopoverTrigger: ({ children }: React.PropsWithChildren) =>
    React.createElement(React.Fragment, null, children),
  PopoverContent: ({ children }: React.PropsWithChildren) =>
    React.createElement('div', null, children),
}));

vi.mock('@renderer/components/ui/tooltip', () => ({
  Tooltip: ({ children }: React.PropsWithChildren) => React.createElement(React.Fragment, null, children),
  TooltipTrigger: ({ children }: React.PropsWithChildren) =>
    React.createElement(React.Fragment, null, children),
  TooltipContent: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
}));

vi.mock('@renderer/components/extensions/common/SearchInput', () => ({
  SearchInput: ({
    value,
    onChange,
    placeholder,
  }: {
    value: string;
    onChange: (value: string) => void;
    placeholder?: string;
  }) =>
    React.createElement('input', {
      value,
      placeholder,
      onChange: (event: React.ChangeEvent<HTMLInputElement>) => onChange(event.target.value),
    }),
}));

vi.mock('@renderer/components/extensions/skills/SkillDetailDialog', () => ({
  SkillDetailDialog: () => null,
}));

vi.mock('@renderer/components/extensions/skills/SkillEditorDialog', () => ({
  SkillEditorDialog: ({ allowCodexRootKind }: { allowCodexRootKind: boolean }) =>
    React.createElement('div', {
      'data-testid': 'skill-editor-dialog',
      'data-allow-codex-root-kind': String(allowCodexRootKind),
    }),
}));

vi.mock('@renderer/components/extensions/skills/SkillImportDialog', () => ({
  SkillImportDialog: ({ allowCodexRootKind }: { allowCodexRootKind: boolean }) =>
    React.createElement('div', {
      'data-testid': 'skill-import-dialog',
      'data-allow-codex-root-kind': String(allowCodexRootKind),
    }),
}));

vi.mock('lucide-react', () => {
  const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
  return {
    AlertTriangle: Icon,
    ArrowUpAZ: Icon,
    ArrowUpDown: Icon,
    BookOpen: Icon,
    Check: Icon,
    CheckCircle2: Icon,
    Clock3: Icon,
    Download: Icon,
    Plus: Icon,
    Search: Icon,
  };
});

import { SkillsPanel } from '@renderer/components/extensions/skills/SkillsPanel';

function makeUserSkill(): SkillCatalogItem {
  return {
    id: '/Users/me/.claude/skills/review-helper',
    sourceType: 'filesystem',
    name: 'Review Helper',
    description: 'Helps with code review',
    folderName: 'review-helper',
    scope: 'user',
    rootKind: 'claude',
    projectRoot: null,
    discoveryRoot: '/Users/me/.claude/skills',
    skillDir: '/Users/me/.claude/skills/review-helper',
    skillFile: '/Users/me/.claude/skills/review-helper/SKILL.md',
    metadata: {},
    invocationMode: 'auto',
    flags: {
      hasScripts: false,
      hasReferences: false,
      hasAssets: false,
    },
    isValid: true,
    issues: [],
    modifiedAt: 1,
  };
}

function makeCodexSkill(): SkillCatalogItem {
  return {
    ...makeUserSkill(),
    id: '/Users/me/.codex/skills/codex-helper',
    name: 'Codex Helper',
    description: 'Helps only Codex sessions',
    folderName: 'codex-helper',
    rootKind: 'codex',
    discoveryRoot: '/Users/me/.codex/skills',
    skillDir: '/Users/me/.codex/skills/codex-helper',
    skillFile: '/Users/me/.codex/skills/codex-helper/SKILL.md',
  };
}

function makeMultimodelStatus(
  overrides?: Partial<CliInstallationStatus>
): CliInstallationStatus {
  return {
    flavor: 'agent_teams_orchestrator',
    displayName: 'Multimodel runtime',
    supportsSelfUpdate: false,
    showVersionDetails: true,
    showBinaryPath: true,
    installed: true,
    installedVersion: '1.0.0',
    binaryPath: '/usr/local/bin/agent-teams',
    latestVersion: '1.0.0',
    updateAvailable: false,
    authLoggedIn: false,
    authStatusChecking: false,
    authMethod: null,
    providers: [
      {
        providerId: 'anthropic',
        displayName: 'Anthropic',
        supported: true,
        authenticated: true,
        authMethod: 'oauth',
        verificationState: 'verified',
        statusMessage: 'Connected',
        models: [],
        canLoginFromUi: true,
        capabilities: {
          teamLaunch: true,
          oneShot: true,
          extensions: createDefaultCliExtensionCapabilities({
            plugins: { status: 'supported', ownership: 'provider-scoped', reason: null },
          }),
        },
        connection: null,
        backend: null,
      },
    ],
    ...overrides,
  };
}

describe('SkillsPanel', () => {
  beforeEach(() => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    storeState.fetchSkillsCatalog = vi.fn().mockResolvedValue(undefined);
    storeState.fetchSkillDetail = vi.fn().mockResolvedValue(undefined);
    storeState.skillsCatalogLoadingByProjectPath = {};
    storeState.skillsCatalogErrorByProjectPath = {};
    storeState.skillsDetailsById = {};
    storeState.skillsUserCatalog = [makeUserSkill()];
    storeState.skillsProjectCatalogByProjectPath = {
      '/tmp/project-a': [],
    };
    storeState.cliStatusLoading = false;
    storeState.appConfig = {
      general: {
        multimodelEnabled: true,
      },
    };
    storeState.cliStatus = {
      flavor: 'claude',
      displayName: 'Claude CLI',
      supportsSelfUpdate: true,
      showVersionDetails: true,
      showBinaryPath: true,
      installed: true,
      installedVersion: '1.0.0',
      binaryPath: '/usr/local/bin/claude',
      latestVersion: '1.0.0',
      updateAvailable: false,
      authLoggedIn: true,
      authStatusChecking: false,
      authMethod: 'oauth',
      providers: [],
    };
    codexAccountHookState.snapshot = null;
    codexAccountHookState.loading = false;
    codexAccountHookState.error = null;
    startWatchingMock.mockReset();
    stopWatchingMock.mockReset();
    onChangedMock.mockReset();
    skillsChangedHandler = null;
    startWatchingMock.mockResolvedValue('watch-1');
    onChangedMock.mockImplementation((handler: typeof skillsChangedHandler) => {
      skillsChangedHandler = handler;
      return () => {
        skillsChangedHandler = null;
      };
    });
  });

  afterEach(() => {
    document.body.innerHTML = '';
    vi.unstubAllGlobals();
  });

  it('refetches personal skill details without forcing the current project path', async () => {
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);
    const skill = storeState.skillsUserCatalog[0]!;

    await act(async () => {
      root.render(
        React.createElement(SkillsPanel, {
          projectPath: '/tmp/project-a',
          projectLabel: 'Project A',
          skillsSearchQuery: '',
          setSkillsSearchQuery: vi.fn(),
          skillsSort: 'name-asc',
          setSkillsSort: vi.fn(),
          selectedSkillId: skill.id,
          setSelectedSkillId: vi.fn(),
        })
      );
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(startWatchingMock).toHaveBeenCalledWith('/tmp/project-a');
    expect(skillsChangedHandler).not.toBeNull();

    await act(async () => {
      skillsChangedHandler?.({
        scope: 'user',
        projectPath: null,
        path: `${skill.skillDir}/SKILL.md`,
        type: 'change',
      });
      await Promise.resolve();
    });

    expect(storeState.fetchSkillsCatalog).toHaveBeenCalledWith('/tmp/project-a');
    expect(storeState.fetchSkillDetail).toHaveBeenCalledWith(skill.id, undefined);

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('hides codex-only create and import affordances when codex runtime is unavailable', async () => {
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(SkillsPanel, {
          projectPath: '/tmp/project-a',
          projectLabel: 'Project A',
          skillsSearchQuery: '',
          setSkillsSearchQuery: vi.fn(),
          skillsSort: 'name-asc',
          setSkillsSort: vi.fn(),
          selectedSkillId: null,
          setSelectedSkillId: vi.fn(),
        })
      );
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(host.textContent).not.toContain('Codex only');
    for (const node of host.querySelectorAll('[data-testid="skill-editor-dialog"]')) {
      expect(node.getAttribute('data-allow-codex-root-kind')).toBe('false');
    }
    const importDialog = host.querySelector('[data-testid="skill-import-dialog"]');
    expect(importDialog?.getAttribute('data-allow-codex-root-kind')).toBe('false');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('uses a runtime-aware shared skills banner when codex is unavailable', async () => {
    storeState.cliStatus = makeMultimodelStatus();
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(SkillsPanel, {
          projectPath: '/tmp/project-a',
          projectLabel: 'Project A',
          skillsSearchQuery: '',
          setSkillsSearchQuery: vi.fn(),
          skillsSort: 'name-asc',
          setSkillsSort: vi.fn(),
          selectedSkillId: null,
          setSelectedSkillId: vi.fn(),
        })
      );
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic.'
    );
    expect(host.textContent).not.toContain('available to both Anthropic and Codex');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('uses the live Codex snapshot to expose Codex-only skill affordances after a stale provider bootstrap', async () => {
    storeState.cliStatus = makeMultimodelStatus({
      providers: [
        ...makeMultimodelStatus().providers,
        {
          providerId: 'codex',
          displayName: 'Codex',
          supported: false,
          authenticated: false,
          authMethod: null,
          verificationState: 'unknown',
          statusMessage: 'Checking...',
          models: [],
          canLoginFromUi: false,
          capabilities: {
            teamLaunch: true,
            oneShot: true,
            extensions: createDefaultCliExtensionCapabilities({
              plugins: { status: 'unsupported', ownership: 'provider-scoped', reason: null },
            }),
          },
          connection: null,
          backend: null,
        },
      ],
    });
    codexAccountHookState.snapshot = {
      preferredAuthMode: 'chatgpt',
      effectiveAuthMode: 'chatgpt',
      launchAllowed: true,
      launchIssueMessage: null,
      launchReadinessState: 'ready_chatgpt',
      appServerState: 'healthy',
      appServerStatusMessage: null,
      managedAccount: {
        type: 'chatgpt',
        email: 'user@example.com',
        planType: 'pro',
      },
      apiKey: {
        available: true,
        source: 'environment',
        sourceLabel: 'Detected from OPENAI_API_KEY',
      },
      requiresOpenaiAuth: false,
      login: {
        status: 'idle',
        error: null,
        startedAt: null,
      },
      rateLimits: null,
      updatedAt: new Date().toISOString(),
    };

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(SkillsPanel, {
          projectPath: '/tmp/project-a',
          projectLabel: 'Project A',
          skillsSearchQuery: '',
          setSkillsSearchQuery: vi.fn(),
          skillsSort: 'name-asc',
          setSkillsSort: vi.fn(),
          selectedSkillId: null,
          setSelectedSkillId: vi.fn(),
        })
      );
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic and Codex.'
    );
    expect(host.textContent).toContain('Codex only');
    expect(host.textContent).toContain('Use `.codex` when a skill should stay Codex-only.');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('uses the live Codex snapshot even while multimodel provider status is still loading', async () => {
    storeState.cliStatus = null;
    storeState.cliStatusLoading = true;
    codexAccountHookState.snapshot = {
      preferredAuthMode: 'chatgpt',
      effectiveAuthMode: 'chatgpt',
      launchAllowed: true,
      launchIssueMessage: null,
      launchReadinessState: 'ready_chatgpt',
      appServerState: 'healthy',
      appServerStatusMessage: null,
      managedAccount: {
        type: 'chatgpt',
        email: 'user@example.com',
        planType: 'pro',
      },
      apiKey: {
        available: true,
        source: 'environment',
        sourceLabel: 'Detected from OPENAI_API_KEY',
      },
      requiresOpenaiAuth: false,
      login: {
        status: 'idle',
        error: null,
        startedAt: null,
      },
      rateLimits: null,
      updatedAt: new Date().toISOString(),
    };

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(SkillsPanel, {
          projectPath: '/tmp/project-a',
          projectLabel: 'Project A',
          skillsSearchQuery: '',
          setSkillsSearchQuery: vi.fn(),
          skillsSort: 'name-asc',
          setSkillsSort: vi.fn(),
          selectedSkillId: null,
          setSelectedSkillId: vi.fn(),
        })
      );
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(host.textContent).toContain(
      'Shared skills in `.claude`, `.cursor`, and `.agents` are available to Anthropic, Codex, and OpenCode (200+ models).'
    );
    expect(host.textContent).toContain('Codex only');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });

  it('resets the codex-only quick filter when codex entries disappear', async () => {
    storeState.cliStatus = makeMultimodelStatus({
      providers: [
        ...makeMultimodelStatus().providers,
        {
          providerId: 'codex',
          displayName: 'Codex',
          supported: true,
          authenticated: true,
          authMethod: 'api_key',
          verificationState: 'verified',
          statusMessage: 'Connected',
          models: [],
          canLoginFromUi: true,
          capabilities: {
            teamLaunch: true,
            oneShot: true,
            extensions: createDefaultCliExtensionCapabilities({
              plugins: { status: 'unsupported', ownership: 'provider-scoped', reason: null },
            }),
          },
          connection: null,
          backend: null,
        },
      ],
    });
    storeState.skillsUserCatalog = [makeUserSkill(), makeCodexSkill()];

    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        React.createElement(SkillsPanel, {
          projectPath: '/tmp/project-a',
          projectLabel: 'Project A',
          skillsSearchQuery: '',
          setSkillsSearchQuery: vi.fn(),
          skillsSort: 'name-asc',
          setSkillsSort: vi.fn(),
          selectedSkillId: null,
          setSelectedSkillId: vi.fn(),
        })
      );
      await Promise.resolve();
      await Promise.resolve();
    });

    const codexOnlyButton = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('Codex only')
    );
    expect(codexOnlyButton).toBeDefined();

    await act(async () => {
      (codexOnlyButton as HTMLButtonElement).click();
      await Promise.resolve();
    });

    expect(host.textContent).toContain('Codex Helper');
    expect(host.textContent).not.toContain('Review Helper');

    storeState.cliStatus = {
      ...storeState.cliStatus,
      providers: storeState.cliStatus.providers.filter((provider) => provider.providerId !== 'codex'),
    };
    storeState.skillsUserCatalog = [makeUserSkill()];

    await act(async () => {
      root.render(
        React.createElement(SkillsPanel, {
          projectPath: '/tmp/project-a',
          projectLabel: 'Project A',
          skillsSearchQuery: '',
          setSkillsSearchQuery: vi.fn(),
          skillsSort: 'name-asc',
          setSkillsSort: vi.fn(),
          selectedSkillId: null,
          setSelectedSkillId: vi.fn(),
        })
      );
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(host.textContent).toContain('Review Helper');
    expect(host.textContent).not.toContain('Codex Helper');
    expect(host.textContent).not.toContain('No skills yet');

    await act(async () => {
      root.unmount();
      await Promise.resolve();
    });
  });
});
