import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor, act } from '@testing-library/react-native';
import React, { type PropsWithChildren } from 'react';

import { AuthError } from '@/core/api/types';

import {
  useSession,
  useLogin,
  useLogout,
  authKeys,
} from '@/features/auth/hooks';

// ─── Mocks ───

const mockHasTokens = jest.fn<Promise<boolean>, []>();
const mockGetRefreshToken = jest.fn<Promise<string | null>, []>();
const mockSetTokens = jest.fn<Promise<void>, [string, string]>();
const mockClearTokens = jest.fn<Promise<void>, []>();

jest.mock('@/core/auth/token-manager', () => ({
  tokenManager: {
    hasTokens: () => mockHasTokens(),
    getAccessToken: jest.fn().mockResolvedValue('access-token'),
    getRefreshToken: () => mockGetRefreshToken(),
    setTokens: (...args: [string, string]) => mockSetTokens(...args),
    clearTokens: () => mockClearTokens(),
  },
}));

const mockFetchMe = jest.fn();
const mockLogin = jest.fn();
const mockLogout = jest.fn();

jest.mock('@/features/auth/api', () => ({
  authApi: {
    fetchMe: () => mockFetchMe(),
    login: (body: unknown) => mockLogin(body),
    logout: (token: string) => mockLogout(token),
  },
}));

// ─── Helpers ───

let queryClient: QueryClient;

function createWrapper() {
  queryClient = new QueryClient({
    defaultOptions: {
      queries: { gcTime: Infinity, retryDelay: 0 },
      mutations: { retry: false },
    },
  });

  return function Wrapper({ children }: PropsWithChildren) {
    return (
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    );
  };
}

afterEach(async () => {
  await queryClient?.cancelQueries();
  queryClient?.clear();
  jest.clearAllMocks();
});

const fakeUser = {
  id: '1',
  phone: '13800138000',
  email: null,
  nickname: 'Test',
  avatar_url: null,
  subscription_type: 'free',
  created_at: '2026-01-01T00:00:00Z',
};

// ─── useSession ───

describe('useSession', () => {
  test('returns loading while tokenCheck is pending', () => {
    mockHasTokens.mockReturnValue(new Promise(() => {}));

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

    expect(result.current.status).toBe('loading');
    expect(result.current.user).toBeNull();
    expect(result.current.isAuthenticated).toBe(false);
  });

  test('returns unauthenticated when no tokens exist', async () => {
    mockHasTokens.mockResolvedValue(false);

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

    await waitFor(() => {
      expect(result.current.status).toBe('unauthenticated');
    });

    expect(result.current.user).toBeNull();
    expect(result.current.isAuthenticated).toBe(false);
    expect(mockFetchMe).not.toHaveBeenCalled();
  });

  test('returns authenticated when tokens exist and /me succeeds', async () => {
    mockHasTokens.mockResolvedValue(true);
    mockFetchMe.mockResolvedValue(fakeUser);

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

    await waitFor(() => {
      expect(result.current.status).toBe('authenticated');
    });

    expect(result.current.user).toEqual(fakeUser);
    expect(result.current.isAuthenticated).toBe(true);
  });

  test('returns unauthenticated when /me throws AuthError', async () => {
    mockHasTokens.mockResolvedValue(true);
    mockFetchMe.mockRejectedValue(new AuthError());

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

    await waitFor(() => {
      expect(result.current.status).toBe('unauthenticated');
    });

    expect(result.current.isAuthenticated).toBe(false);
  });

  test('returns authenticated (optimistic) when /me throws non-auth error', async () => {
    mockHasTokens.mockResolvedValue(true);
    mockFetchMe.mockRejectedValue(new Error('Server 500'));

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

    await waitFor(
      () => {
        expect(result.current.status).toBe('authenticated');
      },
      { timeout: 5000 },
    );

    expect(result.current.isAuthenticated).toBe(true);
    expect(result.current.user).toBeNull();
  });

  test('returns authenticated (optimistic) when tokens exist and /me is loading', async () => {
    mockHasTokens.mockResolvedValue(true);
    mockFetchMe.mockReturnValue(new Promise(() => {})); // never resolves

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

    await waitFor(() => {
      expect(result.current.status).toBe('authenticated');
    });

    expect(result.current.isAuthenticated).toBe(true);
    expect(result.current.user).toBeNull();
  });

  test('returns error when tokenCheck itself fails (SecureStore crash)', async () => {
    mockHasTokens.mockRejectedValue(new Error('SecureStore unavailable'));

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

    await waitFor(() => {
      expect(result.current.status).toBe('error');
    });

    expect(result.current.isAuthenticated).toBe(false);
    expect(mockFetchMe).not.toHaveBeenCalled();
  });
});

// ─── useLogin ───

describe('useLogin', () => {
  test('sets tokens and updates tokenCheck cache on success', async () => {
    const tokens = {
      access_token: 'new-access',
      refresh_token: 'new-refresh',
      token_type: 'bearer',
    };
    mockLogin.mockResolvedValue(tokens);
    mockHasTokens.mockResolvedValue(false);

    const wrapper = createWrapper();
    const { result } = renderHook(() => useLogin(), { wrapper });

    await act(async () => {
      result.current.mutate({
        phone: '13800138000',
        password: '123456',
        agreed_to_terms: true,
      });
    });

    await waitFor(() => {
      expect(result.current.isSuccess).toBe(true);
    });

    expect(mockSetTokens).toHaveBeenCalledWith('new-access', 'new-refresh');
    expect(queryClient.getQueryData(authKeys.tokenCheck)).toBe(true);
  });
});

// ─── useLogout ───

describe('useLogout', () => {
  test('reads refresh token first, calls API, then clears local state', async () => {
    mockGetRefreshToken.mockResolvedValue('my-refresh');
    mockLogout.mockResolvedValue({ message: 'ok' });

    const wrapper = createWrapper();

    queryClient.setQueryData(authKeys.tokenCheck, true);
    queryClient.setQueryData(authKeys.session, fakeUser);

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

    await act(async () => {
      result.current.mutate();
    });

    await waitFor(() => {
      expect(result.current.isSuccess).toBe(true);
    });

    expect(mockGetRefreshToken).toHaveBeenCalled();
    expect(mockLogout).toHaveBeenCalledWith('my-refresh');
    expect(mockClearTokens).toHaveBeenCalled();
    expect(queryClient.getQueryData(authKeys.tokenCheck)).toBe(false);
  });

  test('clears local state even if server logout fails', async () => {
    mockGetRefreshToken.mockResolvedValue('my-refresh');
    mockLogout.mockRejectedValue(new Error('Network down'));

    const wrapper = createWrapper();

    queryClient.setQueryData(authKeys.tokenCheck, true);
    queryClient.setQueryData(authKeys.session, fakeUser);

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

    await act(async () => {
      result.current.mutate();
    });

    await waitFor(() => {
      expect(result.current.isSuccess).toBe(true);
    });

    expect(mockClearTokens).toHaveBeenCalled();
    expect(queryClient.getQueryData(authKeys.tokenCheck)).toBe(false);
  });
});
