import { act, renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { beforeEach, expect, it, vi } from 'vitest';
import type { AuthUser } from '@alqove/types';
import type { Membership } from '@alqove/api-client';
import { useAuthStore } from '@/stores/auth';
import { usePostMessage, useDeleteMessage, MESSAGE_KEYS } from '../use-messages';

const mocks = vi.hoisted(() => ({ post: vi.fn(), remove: vi.fn(), myMemberships: vi.fn(), me: vi.fn() }));
vi.mock('@/lib/api', () => ({ api: { team: { myMemberships: mocks.myMemberships }, auth: { me: mocks.me }, messages: { post: mocks.post, delete: mocks.remove, forStore: () => ({ post: mocks.post, delete: mocks.remove }) } } }));
const user = { id: 'same-user' } as AuthUser;
const memberships: Membership[] = ['a', 'b'].map(store_id => ({ id: store_id, store_id, role: 'owner', capabilities: ['store.admin'], is_exempt: false }));
function deferred<T>() {
  let resolve!: (value: T) => void;
  const promise = new Promise<T>(r => { resolve = r; });
  return { promise, resolve };
}
beforeEach(async () => {
  vi.resetAllMocks();
  useAuthStore.getState().clearAuth();
  mocks.myMemberships.mockResolvedValue({ data: memberships });
  mocks.me.mockResolvedValue({ data: user });
  mocks.post.mockResolvedValue({ data: { id: 'sent' } });
  useAuthStore.getState().setAuth(user, 'original-token');
  await useAuthStore.getState().refreshMemberships();
});
it.each(['same-user token replacement', 'A → B → A', 'storage identity reload', 'membership removal and restoration'])('rejects a queued old draft after %s', async (transition) => {
  const qc = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
  const pending = deferred<void>();
  vi.spyOn(qc, 'cancelQueries').mockReturnValue(pending.promise);
  const previous = { data: [], meta: { total: 0, has_more: false } };
  qc.setQueryData(MESSAGE_KEYS.thread('order', 'a'), previous);
  const { result, rerender } = renderHook(() => usePostMessage('order', 'a'), { wrapper: ({ children }) => <QueryClientProvider client={qc}>{children}</QueryClientProvider> });
  let work!: Promise<unknown>;
  await act(async () => { work = result.current.mutateAsync({ body: 'old draft' }).catch(error => error); });
  await act(async () => {
    if (transition === 'same-user token replacement') {
      useAuthStore.getState().setAuth(user, 'replacement-token');
      await useAuthStore.getState().refreshMemberships();
    } else if (transition === 'A → B → A') {
      useAuthStore.getState().selectStore('b');
      useAuthStore.getState().selectStore('a');
    } else if (transition === 'storage identity reload') {
      await useAuthStore.getState().loadFromStorage();
    } else {
      mocks.myMemberships.mockResolvedValueOnce({ data: [] });
      await useAuthStore.getState().refreshMemberships();
      await useAuthStore.getState().refreshMemberships();
    }
  });
  rerender();
  await act(async () => { pending.resolve(); await work; });
  expect(mocks.post).not.toHaveBeenCalled();
  expect(await work).toBeInstanceOf(Error);
  expect(qc.getQueryData(MESSAGE_KEYS.thread('order', 'a'))).toEqual(previous);
});

it.each(['same-user token replacement', 'A → B → A'])('settles dispatched work without a stale composer callback after %s', async transition => {
  const qc = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
  const invalidate = vi.spyOn(qc, 'invalidateQueries');
  const pending = deferred<{ data: { id: string } }>();
  mocks.post.mockReturnValue(pending.promise);
  const onSuccess = vi.fn();
  const onSettled = vi.fn();
  const { result } = renderHook(() => usePostMessage('order', 'a'), { wrapper: ({ children }) => <QueryClientProvider client={qc}>{children}</QueryClientProvider> });
  act(() => result.current.mutate({ body: 'already sent' }, { onSuccess, onSettled }));
  await waitFor(() => expect(mocks.post).toHaveBeenCalledOnce());
  await act(async () => {
    if (transition === 'A → B → A') {
      useAuthStore.getState().selectStore('b');
      useAuthStore.getState().selectStore('a');
    } else {
      useAuthStore.getState().setAuth(user, 'replacement-token');
      await useAuthStore.getState().refreshMemberships();
    }
    pending.resolve({ data: { id: 'sent' } });
  });
  await waitFor(() => expect(invalidate).toHaveBeenCalled());
  expect(onSuccess).not.toHaveBeenCalled();
  expect(onSettled).not.toHaveBeenCalled();
  expect(invalidate).toHaveBeenCalledWith({ queryKey: MESSAGE_KEYS.thread('order', 'a'), refetchType: 'none' });
  expect(invalidate).not.toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['notifications', 'unread-count'] }));
});

it.each(['post', 'delete'])('does not revive a retained %s closure after A → B → A', async kind => {
  const qc = new QueryClient();
  const { result } = renderHook(() => ({ post: usePostMessage('order', 'a'), remove: useDeleteMessage('order', 'a') }), { wrapper: ({ children }) => <QueryClientProvider client={qc}>{children}</QueryClientProvider> });
  const oldPost = result.current.post.mutateAsync;
  const oldDelete = result.current.remove.mutateAsync;
  act(() => {
    useAuthStore.getState().selectStore('b');
    useAuthStore.getState().selectStore('a');
  });
  await act(async () => {
    await expect(kind === 'post' ? oldPost({ body: 'old draft' }) : oldDelete('message')).rejects.toThrow('Store access changed');
  });
  expect(mocks.post).not.toHaveBeenCalled();
  expect(mocks.remove).not.toHaveBeenCalled();
});

it('does not overwrite a replacement session cache when an old dispatched post fails', async () => {
  const qc = new QueryClient({ defaultOptions: { mutations: { retry: false } } });
  const pending = deferred<void>();
  mocks.post.mockImplementation(async () => { await pending.promise; throw new Error('network failure'); });
  qc.setQueryData(MESSAGE_KEYS.thread('order', 'a'), { data: [{ id: 'old' }], meta: { total: 1, has_more: false } });
  const { result } = renderHook(() => usePostMessage('order', 'a'), { wrapper: ({ children }) => <QueryClientProvider client={qc}>{children}</QueryClientProvider> });
  act(() => result.current.mutate({ body: 'old draft' }));
  await waitFor(() => expect(mocks.post).toHaveBeenCalledOnce());
  const replacement = { data: [{ id: 'new-session-data' }], meta: { total: 1, has_more: false } };
  await act(async () => {
    useAuthStore.getState().setAuth(user, 'replacement-token');
    await useAuthStore.getState().refreshMemberships();
    qc.setQueryData(MESSAGE_KEYS.thread('order', 'a'), replacement);
    pending.resolve();
  });
  await waitFor(() => expect(result.current.isError).toBe(true));
  expect(qc.getQueryData(MESSAGE_KEYS.thread('order', 'a'))).toEqual(replacement);
});
