import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { getFunctionName } from 'convex/server';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { api } from '@spoon/backend/convex/_generated/api.js';

import { SpoonAgentSettingsForm } from '../../src/components/spoons/spoon-agent-settings-form';

const { mockUseMutation, mockUseQuery } = vi.hoisted(() => ({
  mockUseMutation: vi.fn(),
  mockUseQuery: vi.fn(),
}));

vi.mock('convex/react', () => ({
  useMutation: mockUseMutation,
  useQuery: mockUseQuery,
}));

vi.mock('sonner', () => ({
  toast: {
    error: vi.fn(),
    success: vi.fn(),
  },
}));

const spoon = {
  _id: 'spoon-1',
  forkDefaultBranch: 'main',
  upstreamDefaultBranch: 'main',
} as never;

const configuredProfile = {
  _id: 'profile-1',
  name: 'OpenAI',
  provider: 'openai',
  enabled: true,
  configured: true,
  isDefault: true,
  supportedRuntimes: ['codex'],
  reasoningEffort: 'high',
};

const modelCatalog = {
  profiles: [
    {
      profileId: 'profile-1',
      configured: true,
      defaultModel: 'gpt-5',
      models: [{ id: 'gpt-5', label: 'GPT-5' }],
    },
  ],
};

const loadedSettings = {
  _id: 'settings-1' as never,
  enabled: true,
  runtime: 'codex' as const,
  defaultBaseBranch: 'develop',
  branchPrefix: 'custom/prefix',
  installCommand: 'bun install',
  agentModel: 'gpt-5',
  reasoningEffort: 'high' as const,
  envFilePath: '.env',
  aiProviderProfileId: 'profile-1' as never,
};

const wireQueries = () => {
  const listMineName = getFunctionName(api.aiProviderProfiles.listMine);
  const catalogName = getFunctionName(api.aiProviderModels.listAvailableForUser);
  mockUseQuery.mockImplementation(
    (ref: Parameters<typeof getFunctionName>[0]) => {
      const name = getFunctionName(ref);
      if (name === listMineName) return [configuredProfile];
      if (name === catalogName) return modelCatalog;
      return undefined;
    },
  );
};

afterEach(() => {
  cleanup();
  vi.clearAllMocks();
});

describe('SpoonAgentSettingsForm re-sync on loaded record', () => {
  it('reflects the loaded settings after the record resolves late', async () => {
    wireQueries();
    mockUseMutation.mockReturnValue(vi.fn());

    // Parent gates on a different query, so this form mounts before the
    // agent settings query resolves (settings === undefined).
    const { rerender } = render(
      <SpoonAgentSettingsForm spoon={spoon} settings={undefined} />,
    );

    expect(screen.getByLabelText(/branch prefix/i)).toHaveValue('spoon/agent');

    // The settings query resolves with a saved record carrying non-defaults.
    rerender(<SpoonAgentSettingsForm spoon={spoon} settings={loadedSettings} />);

    await waitFor(() =>
      expect(screen.getByLabelText(/branch prefix/i)).toHaveValue(
        'custom/prefix',
      ),
    );
    expect(screen.getByLabelText(/default base branch/i)).toHaveValue(
      'develop',
    );
    expect(screen.getByLabelText(/install command/i)).toHaveValue(
      'bun install',
    );
  });

  it('saves the loaded values, not the stale defaults, after a late resolve', async () => {
    wireQueries();
    const update = vi.fn().mockResolvedValue('settings-1');
    mockUseMutation.mockReturnValue(update);

    const { rerender } = render(
      <SpoonAgentSettingsForm spoon={spoon} settings={undefined} />,
    );
    rerender(<SpoonAgentSettingsForm spoon={spoon} settings={loadedSettings} />);

    // Wait for the late record to hydrate the form before saving.
    await waitFor(() =>
      expect(screen.getByLabelText(/branch prefix/i)).toHaveValue(
        'custom/prefix',
      ),
    );

    fireEvent.click(
      screen.getByRole('button', { name: /save agent settings/i }),
    );

    await waitFor(() => expect(update).toHaveBeenCalledTimes(1));
    expect(update).toHaveBeenCalledWith(
      expect.objectContaining({
        branchPrefix: 'custom/prefix',
        defaultBaseBranch: 'develop',
        installCommand: 'bun install',
        agentModel: 'gpt-5',
      }),
    );
  });
});
