import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, renderHook, waitFor } from '@testing-library/react';
import { describe, expect, it, vi, beforeEach } from 'vitest';

import { useObjectDescriptions } from '../src/platform/infrastructure/api/objectMetadataHooks';
import { useCreateSchema } from '../src/platform/infrastructure/api/objectQueries';

/**
 * The hook's status is three-valued. The first cut exposed a boolean `loaded`,
 * and the caller rendered "Loading…" for everything that was not loaded — so a
 * failed first read left every Object Manager card loading forever, with no
 * error and no retry. Codex blocked app-packs#621 on exactly that.
 *
 * These pin the state machine at the source, not just at the render site: the
 * caller can only be correct if the hook tells it the truth.
 */

const apiMocks = vi.hoisted(() => ({
  getObjectAuthoringMetadataList: vi.fn(),
  createSchema: vi.fn(),
}));

vi.mock('../src/platform/infrastructure/api/metadata', () => ({
  metadataAPI: apiMocks,
}));

function wrapper() {
  const client = new QueryClient({ defaultOptions: { queries: { retryDelay: 0 } } });
  return function Wrapper({ children }: { children: React.ReactNode }) {
    return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
  };
}

vi.mock('../src/platform/infrastructure/api/searchMetadata', () => ({
  searchMetadata: { refresh: () => Promise.resolve() },
}));

beforeEach(() => {
  vi.clearAllMocks();
  apiMocks.createSchema.mockResolvedValue({ schema: { name: 'New__c' }, message: 'ok' });
});

describe('useObjectDescriptions status', () => {
  it('reports loading while the request is in flight, then loaded', async () => {
    let resolve: (v: unknown) => void = () => {};
    apiMocks.getObjectAuthoringMetadataList.mockReturnValue(new Promise(r => { resolve = r; }));

    const { result } = renderHook(() => useObjectDescriptions(), { wrapper: wrapper() });
    expect(result.current.status).toBe('loading');

    resolve([{ name: 'Account', description: 'Standard Salesforce Account object' }]);

    await waitFor(() => expect(result.current.status).toBe('loaded'));
    expect(result.current.descriptions?.get('account')).toBe('Standard Salesforce Account object');
  });

  it('reports unavailable — never loading — when the first read fails', async () => {
    apiMocks.getObjectAuthoringMetadataList.mockRejectedValue(new Error('boom'));

    const { result } = renderHook(() => useObjectDescriptions(), { wrapper: wrapper() });

    await waitFor(() => expect(result.current.status).toBe('unavailable'));
    // The bug: a settled failure that still claims to be loading.
    expect(result.current.status).not.toBe('loading');
    expect(result.current.descriptions).toBeNull();
  });

  it('keys the map lowercase so lookups do not depend on wire casing', async () => {
    apiMocks.getObjectAuthoringMetadataList.mockResolvedValue([
      { name: 'Order_Product__c', description: 'RETIRED — superseded by OrderItem' },
    ]);

    const { result } = renderHook(() => useObjectDescriptions(), { wrapper: wrapper() });

    await waitFor(() => expect(result.current.status).toBe('loaded'));
    expect(result.current.descriptions?.get('order_product__c')).toBe('RETIRED — superseded by OrderItem');
  });

  it('stays loaded when a refetch fails, since the cached descriptions are still real', async () => {
    apiMocks.getObjectAuthoringMetadataList.mockResolvedValueOnce([
      { name: 'Account', description: 'first' },
    ]);
    const { result } = renderHook(() => useObjectDescriptions(), { wrapper: wrapper() });
    await waitFor(() => expect(result.current.status).toBe('loaded'));

    apiMocks.getObjectAuthoringMetadataList.mockRejectedValue(new Error('boom'));
    await act(async () => {
      await result.current.refresh();
    });

    // Wait for the failure to be observable first. Asserting straight after the
    // await read a pre-re-render snapshot, so the assertion below held no
    // matter what the hook did — it passed even with the guard mutated away.
    await waitFor(() => expect(result.current.error).toBeTruthy());

    // Downgrading to 'unavailable' here would blank out descriptions that are
    // on screen and still accurate.
    expect(result.current.status).toBe('loaded');
    expect(result.current.descriptions?.get('account')).toBe('first');
  });
});

describe('descriptions cache invalidation', () => {
  it('refetches descriptions after an object is created', async () => {
    // The descriptions query has a 10-minute staleTime and a different key
    // prefix from OBJECT_KEYS, so the pre-existing schema invalidation does not
    // reach it. Without this the cards keep showing the pre-mutation set.
    apiMocks.getObjectAuthoringMetadataList
      .mockResolvedValueOnce([{ name: 'Account', description: 'before' }])
      .mockResolvedValueOnce([
        { name: 'Account', description: 'before' },
        { name: 'New__c', description: 'after' },
      ]);

    const client = new QueryClient({ defaultOptions: { queries: { retryDelay: 0 } } });
    const Wrapper = ({ children }: { children: React.ReactNode }) => (
      <QueryClientProvider client={client}>{children}</QueryClientProvider>
    );

    const { result } = renderHook(() => useObjectDescriptions(), { wrapper: Wrapper });
    await waitFor(() => expect(result.current.status).toBe('loaded'));
    expect(result.current.descriptions?.has('new__c')).toBe(false);

    // Drive the real mutation hook. Calling invalidateQueries directly here
    // would test the query client, not that useCreateSchema actually reaches
    // for this key — the mutation could drop it and the test would stay green.
    const { result: create } = renderHook(() => useCreateSchema(), { wrapper: Wrapper });
    await act(async () => {
      await create.current.mutateAsync({ name: 'New__c', label: 'New' });
    });

    await waitFor(() => expect(result.current.descriptions?.get('new__c')).toBe('after'));
  });
});
