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

// Mock apiClient
vi.mock('../../api/client', () => ({
  default: {
    get: vi.fn(),
    post: vi.fn(),
    delete: vi.fn(),
    patch: vi.fn(),
  },
}));

import {
  useApiTokens,
  useCreateApiToken,
  useRevokeApiToken,
  useUpdateApiToken,
  useTestTokensForDocs,
  API_SCOPES,
  SCOPE_PRESETS,
} from '../useApiTokens';
import type {
  APIToken,
  APITokenCreateResponse,
  CreateTokenData,
  TestTokenForDocs,
  APIScope,
} from '../useApiTokens';
import apiClient from '../../api/client';

// Create a wrapper with QueryClientProvider
const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
      mutations: {
        retry: false,
      },
    },
  });

  return function Wrapper({ children }: { children: React.ReactNode }) {
    return React.createElement(
      QueryClientProvider,
      { client: queryClient },
      children
    );
  };
};

// Mock data
const mockApiToken: APIToken = {
  id: 'token-123',
  name: 'Test Token',
  key_prefix: 'ss_test',
  scopes: ['services:read', 'bookings:write'],
  is_active: true,
  is_sandbox: false,
  created_at: '2024-01-01T00:00:00Z',
  last_used_at: '2024-01-15T12:30:00Z',
  expires_at: null,
  created_by: {
    id: 1,
    username: 'testuser',
    full_name: 'Test User',
  },
};

const mockApiTokenCreateResponse: APITokenCreateResponse = {
  ...mockApiToken,
  key: 'ss_test_1234567890abcdef',
};

const mockTestToken: TestTokenForDocs = {
  id: 'test-token-123',
  name: 'Test Token for Docs',
  key_prefix: 'ss_test',
  created_at: '2024-01-01T00:00:00Z',
};

describe('useApiTokens hooks', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('useApiTokens', () => {
    it('fetches API tokens successfully', async () => {
      const mockTokens = [mockApiToken];
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockTokens });

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

      expect(result.current.isLoading).toBe(true);

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

      expect(result.current.data).toEqual(mockTokens);
      expect(apiClient.get).toHaveBeenCalledWith('/v1/tokens/');
    });

    it('handles empty token list', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [] });

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

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

      expect(result.current.data).toEqual([]);
    });

    it('handles fetch error', async () => {
      const mockError = new Error('Failed to fetch tokens');
      vi.mocked(apiClient.get).mockRejectedValue(mockError);

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

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

      expect(result.current.error).toEqual(mockError);
    });

    it('returns multiple tokens correctly', async () => {
      const mockTokens = [
        mockApiToken,
        {
          ...mockApiToken,
          id: 'token-456',
          name: 'Production Token',
          is_sandbox: false,
        },
        {
          ...mockApiToken,
          id: 'token-789',
          name: 'Sandbox Token',
          is_sandbox: true,
        },
      ];
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockTokens });

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

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

      expect(result.current.data).toHaveLength(3);
      expect(result.current.data).toEqual(mockTokens);
    });
  });

  describe('useCreateApiToken', () => {
    it('creates API token successfully', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: mockApiTokenCreateResponse });

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

      const createData: CreateTokenData = {
        name: 'Test Token',
        scopes: ['services:read', 'bookings:write'],
      };

      let response: APITokenCreateResponse | undefined;
      await act(async () => {
        response = await result.current.mutateAsync(createData);
      });

      expect(apiClient.post).toHaveBeenCalledWith('/v1/tokens/', createData);
      expect(response).toEqual(mockApiTokenCreateResponse);
      expect(response?.key).toBe('ss_test_1234567890abcdef');
    });

    it('creates token with expiration date', async () => {
      const expiresAt = '2024-12-31T23:59:59Z';
      const tokenWithExpiry = {
        ...mockApiTokenCreateResponse,
        expires_at: expiresAt,
      };

      vi.mocked(apiClient.post).mockResolvedValue({ data: tokenWithExpiry });

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

      const createData: CreateTokenData = {
        name: 'Expiring Token',
        scopes: ['services:read'],
        expires_at: expiresAt,
      };

      let response: APITokenCreateResponse | undefined;
      await act(async () => {
        response = await result.current.mutateAsync(createData);
      });

      expect(apiClient.post).toHaveBeenCalledWith('/v1/tokens/', createData);
      expect(response?.expires_at).toBe(expiresAt);
    });

    it('creates sandbox token', async () => {
      const sandboxToken = {
        ...mockApiTokenCreateResponse,
        is_sandbox: true,
        key_prefix: 'ss_test',
      };

      vi.mocked(apiClient.post).mockResolvedValue({ data: sandboxToken });

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

      const createData: CreateTokenData = {
        name: 'Sandbox Token',
        scopes: ['services:read'],
        is_sandbox: true,
      };

      let response: APITokenCreateResponse | undefined;
      await act(async () => {
        response = await result.current.mutateAsync(createData);
      });

      expect(apiClient.post).toHaveBeenCalledWith('/v1/tokens/', createData);
      expect(response?.is_sandbox).toBe(true);
    });

    it('invalidates token list after successful creation', async () => {
      vi.mocked(apiClient.post).mockResolvedValue({ data: mockApiTokenCreateResponse });
      vi.mocked(apiClient.get).mockResolvedValue({ data: [mockApiToken] });

      const wrapper = createWrapper();
      const { result: tokenListResult } = renderHook(() => useApiTokens(), { wrapper });
      const { result: createResult } = renderHook(() => useCreateApiToken(), { wrapper });

      // Wait for initial fetch
      await waitFor(() => {
        expect(tokenListResult.current.isSuccess).toBe(true);
      });

      const initialCallCount = vi.mocked(apiClient.get).mock.calls.length;

      // Create new token
      await act(async () => {
        await createResult.current.mutateAsync({
          name: 'New Token',
          scopes: ['services:read'],
        });
      });

      // Wait for refetch
      await waitFor(() => {
        expect(vi.mocked(apiClient.get).mock.calls.length).toBeGreaterThan(initialCallCount);
      });
    });

    it('handles creation error', async () => {
      const mockError = new Error('Failed to create token');
      vi.mocked(apiClient.post).mockRejectedValue(mockError);

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync({
            name: 'Test Token',
            scopes: ['services:read'],
          });
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(mockError);
      await waitFor(() => {
        expect(result.current.isError).toBe(true);
      });
    });

    it('creates token with all available scopes', async () => {
      const allScopesToken = {
        ...mockApiTokenCreateResponse,
        scopes: API_SCOPES.map(s => s.value),
      };

      vi.mocked(apiClient.post).mockResolvedValue({ data: allScopesToken });

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

      const createData: CreateTokenData = {
        name: 'Full Access Token',
        scopes: API_SCOPES.map(s => s.value),
      };

      let response: APITokenCreateResponse | undefined;
      await act(async () => {
        response = await result.current.mutateAsync(createData);
      });

      expect(response?.scopes).toHaveLength(API_SCOPES.length);
    });
  });

  describe('useRevokeApiToken', () => {
    it('revokes API token successfully', async () => {
      vi.mocked(apiClient.delete).mockResolvedValue({});

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

      await act(async () => {
        await result.current.mutateAsync('token-123');
      });

      expect(apiClient.delete).toHaveBeenCalledWith('/v1/tokens/token-123/');
    });

    it('invalidates token list after successful revocation', async () => {
      vi.mocked(apiClient.delete).mockResolvedValue({});
      vi.mocked(apiClient.get).mockResolvedValue({ data: [mockApiToken] });

      const wrapper = createWrapper();
      const { result: tokenListResult } = renderHook(() => useApiTokens(), { wrapper });
      const { result: revokeResult } = renderHook(() => useRevokeApiToken(), { wrapper });

      // Wait for initial fetch
      await waitFor(() => {
        expect(tokenListResult.current.isSuccess).toBe(true);
      });

      const initialCallCount = vi.mocked(apiClient.get).mock.calls.length;

      // Revoke token
      await act(async () => {
        await revokeResult.current.mutateAsync('token-123');
      });

      // Wait for refetch
      await waitFor(() => {
        expect(vi.mocked(apiClient.get).mock.calls.length).toBeGreaterThan(initialCallCount);
      });
    });

    it('handles revocation error', async () => {
      const mockError = new Error('Failed to revoke token');
      vi.mocked(apiClient.delete).mockRejectedValue(mockError);

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync('token-123');
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(mockError);
      await waitFor(() => {
        expect(result.current.isError).toBe(true);
      });
    });
  });

  describe('useUpdateApiToken', () => {
    it('updates API token successfully', async () => {
      const updatedToken = {
        ...mockApiToken,
        name: 'Updated Token Name',
      };

      vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });

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

      let response: APIToken | undefined;
      await act(async () => {
        response = await result.current.mutateAsync({
          tokenId: 'token-123',
          data: { name: 'Updated Token Name' },
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
        name: 'Updated Token Name',
      });
      expect(response?.name).toBe('Updated Token Name');
    });

    it('updates token scopes', async () => {
      const updatedToken = {
        ...mockApiToken,
        scopes: ['services:read', 'bookings:read', 'customers:read'],
      };

      vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });

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

      await act(async () => {
        await result.current.mutateAsync({
          tokenId: 'token-123',
          data: { scopes: ['services:read', 'bookings:read', 'customers:read'] },
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
        scopes: ['services:read', 'bookings:read', 'customers:read'],
      });
    });

    it('deactivates token', async () => {
      const deactivatedToken = {
        ...mockApiToken,
        is_active: false,
      };

      vi.mocked(apiClient.patch).mockResolvedValue({ data: deactivatedToken });

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

      let response: APIToken | undefined;
      await act(async () => {
        response = await result.current.mutateAsync({
          tokenId: 'token-123',
          data: { is_active: false },
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
        is_active: false,
      });
      expect(response?.is_active).toBe(false);
    });

    it('updates token expiration', async () => {
      const newExpiry = '2025-12-31T23:59:59Z';
      const updatedToken = {
        ...mockApiToken,
        expires_at: newExpiry,
      };

      vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });

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

      await act(async () => {
        await result.current.mutateAsync({
          tokenId: 'token-123',
          data: { expires_at: newExpiry },
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
        expires_at: newExpiry,
      });
    });

    it('invalidates token list after successful update', async () => {
      const updatedToken = { ...mockApiToken, name: 'Updated' };
      vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });
      vi.mocked(apiClient.get).mockResolvedValue({ data: [mockApiToken] });

      const wrapper = createWrapper();
      const { result: tokenListResult } = renderHook(() => useApiTokens(), { wrapper });
      const { result: updateResult } = renderHook(() => useUpdateApiToken(), { wrapper });

      // Wait for initial fetch
      await waitFor(() => {
        expect(tokenListResult.current.isSuccess).toBe(true);
      });

      const initialCallCount = vi.mocked(apiClient.get).mock.calls.length;

      // Update token
      await act(async () => {
        await updateResult.current.mutateAsync({
          tokenId: 'token-123',
          data: { name: 'Updated' },
        });
      });

      // Wait for refetch
      await waitFor(() => {
        expect(vi.mocked(apiClient.get).mock.calls.length).toBeGreaterThan(initialCallCount);
      });
    });

    it('handles update error', async () => {
      const mockError = new Error('Failed to update token');
      vi.mocked(apiClient.patch).mockRejectedValue(mockError);

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

      let caughtError;
      await act(async () => {
        try {
          await result.current.mutateAsync({
            tokenId: 'token-123',
            data: { name: 'Updated' },
          });
        } catch (error) {
          caughtError = error;
        }
      });

      expect(caughtError).toEqual(mockError);
      await waitFor(() => {
        expect(result.current.isError).toBe(true);
      });
    });

    it('updates multiple fields at once', async () => {
      const updatedToken = {
        ...mockApiToken,
        name: 'Updated Token',
        scopes: ['services:read', 'bookings:read'],
        expires_at: '2025-12-31T23:59:59Z',
      };

      vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });

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

      await act(async () => {
        await result.current.mutateAsync({
          tokenId: 'token-123',
          data: {
            name: 'Updated Token',
            scopes: ['services:read', 'bookings:read'],
            expires_at: '2025-12-31T23:59:59Z',
          },
        });
      });

      expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
        name: 'Updated Token',
        scopes: ['services:read', 'bookings:read'],
        expires_at: '2025-12-31T23:59:59Z',
      });
    });
  });

  describe('useTestTokensForDocs', () => {
    it('fetches test tokens successfully', async () => {
      const mockTestTokens = [mockTestToken];
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockTestTokens });

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

      expect(result.current.isLoading).toBe(true);

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

      expect(result.current.data).toEqual(mockTestTokens);
      expect(apiClient.get).toHaveBeenCalledWith('/v1/tokens/test-tokens/');
    });

    it('handles empty test token list', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [] });

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

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

      expect(result.current.data).toEqual([]);
    });

    it('handles fetch error', async () => {
      const mockError = new Error('Failed to fetch test tokens');
      vi.mocked(apiClient.get).mockRejectedValue(mockError);

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

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

      expect(result.current.error).toEqual(mockError);
    });

    it('returns multiple test tokens', async () => {
      const mockTestTokens = [
        mockTestToken,
        {
          ...mockTestToken,
          id: 'test-token-456',
          name: 'Another Test Token',
        },
      ];
      vi.mocked(apiClient.get).mockResolvedValue({ data: mockTestTokens });

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

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

      expect(result.current.data).toHaveLength(2);
    });

    it('uses staleTime for caching', async () => {
      vi.mocked(apiClient.get).mockResolvedValue({ data: [mockTestToken] });

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

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

      // Render hook again - should use cached data
      const { result: result2 } = renderHook(() => useTestTokensForDocs(), { wrapper });

      expect(result2.current.data).toEqual([mockTestToken]);
      // Should only call API once due to staleTime cache
      expect(vi.mocked(apiClient.get).mock.calls.length).toBe(1);
    });
  });

  describe('API_SCOPES constant', () => {
    it('contains expected scopes', () => {
      expect(API_SCOPES).toBeDefined();
      expect(Array.isArray(API_SCOPES)).toBe(true);
      expect(API_SCOPES.length).toBeGreaterThan(0);
    });

    it('has correct structure for each scope', () => {
      API_SCOPES.forEach((scope: APIScope) => {
        expect(scope).toHaveProperty('value');
        expect(scope).toHaveProperty('label');
        expect(scope).toHaveProperty('description');
        expect(typeof scope.value).toBe('string');
        expect(typeof scope.label).toBe('string');
        expect(typeof scope.description).toBe('string');
      });
    });

    it('contains essential scopes', () => {
      const scopeValues = API_SCOPES.map(s => s.value);
      expect(scopeValues).toContain('services:read');
      expect(scopeValues).toContain('bookings:read');
      expect(scopeValues).toContain('bookings:write');
      expect(scopeValues).toContain('customers:read');
      expect(scopeValues).toContain('customers:write');
    });
  });

  describe('SCOPE_PRESETS constant', () => {
    it('contains expected presets', () => {
      expect(SCOPE_PRESETS).toBeDefined();
      expect(SCOPE_PRESETS).toHaveProperty('booking_widget');
      expect(SCOPE_PRESETS).toHaveProperty('read_only');
      expect(SCOPE_PRESETS).toHaveProperty('full_access');
    });

    it('booking_widget preset has correct structure', () => {
      const preset = SCOPE_PRESETS.booking_widget;
      expect(preset).toHaveProperty('label');
      expect(preset).toHaveProperty('description');
      expect(preset).toHaveProperty('scopes');
      expect(Array.isArray(preset.scopes)).toBe(true);
      expect(preset.scopes).toContain('services:read');
      expect(preset.scopes).toContain('bookings:write');
    });

    it('read_only preset contains only read scopes', () => {
      const preset = SCOPE_PRESETS.read_only;
      expect(preset.scopes.every(scope => scope.includes(':read'))).toBe(true);
    });

    it('full_access preset contains all scopes', () => {
      const preset = SCOPE_PRESETS.full_access;
      expect(preset.scopes).toHaveLength(API_SCOPES.length);
      expect(preset.scopes).toEqual(API_SCOPES.map(s => s.value));
    });
  });

  describe('TypeScript types', () => {
    it('APIToken type includes all required fields', () => {
      const token: APIToken = mockApiToken;
      expect(token.id).toBeDefined();
      expect(token.name).toBeDefined();
      expect(token.key_prefix).toBeDefined();
      expect(token.scopes).toBeDefined();
      expect(token.is_active).toBeDefined();
      expect(token.is_sandbox).toBeDefined();
      expect(token.created_at).toBeDefined();
    });

    it('APITokenCreateResponse extends APIToken with key', () => {
      const createResponse: APITokenCreateResponse = mockApiTokenCreateResponse;
      expect(createResponse.key).toBeDefined();
      expect(createResponse.id).toBeDefined();
      expect(createResponse.name).toBeDefined();
    });

    it('CreateTokenData has correct structure', () => {
      const createData: CreateTokenData = {
        name: 'Test',
        scopes: ['services:read'],
      };
      expect(createData.name).toBe('Test');
      expect(createData.scopes).toEqual(['services:read']);
    });

    it('TestTokenForDocs has minimal fields', () => {
      const testToken: TestTokenForDocs = mockTestToken;
      expect(testToken.id).toBeDefined();
      expect(testToken.name).toBeDefined();
      expect(testToken.key_prefix).toBeDefined();
      expect(testToken.created_at).toBeDefined();
    });
  });
});
