import React from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { Provider, useAtomValue } from 'jotai';
import { EnviveConfigProvider, useEnviveConfig } from 'src/contexts/enviveConfigContext';
import { LocalStorageProvider } from 'src/contexts/localStorageContext/localStorageContext';
import { GraphQLProvider } from 'src/contexts/graphqlContext/graphqlContext';
import { UserIdentityProvider } from 'src/contexts/userIdentityContext/userIdentityContext';
import {
  FeatureFlagService,
  FeatureFlagServiceProvider,
} from 'src/contexts/featureFlagServiceContext/featureFlagServiceContext';
import { baseUrlAtom } from 'src/atoms/envive/enviveConfig';
import {
  clearSearchServiceFunction,
  setSearchServiceFunction,
} from 'src/atoms/search/searchServiceAdapter';
import { SearchResult } from 'src/application/models/api/search';

import { useAppDetails } from 'src/hooks/AppDetails/useAppDetails';
import * as commerceApiClient from '@spiffy-ai/commerce-api-client';
import { UserIdentityService } from 'src/services/userIdentityService';
import { SearchProvider, useSearchService } from '../searchContext';

// Mock the commerce-api-client
const mockV1SearchQueryGet = vi.fn();
const mockSearchApiConstructor = vi.fn();
const mockConfigurationConstructor = vi.fn();

vi.mock('@spiffy-ai/commerce-api-client', () => {
  const mockV1SearchQueryGet = vi.fn();
  const mockSearchApiConstructor = vi.fn(function SearchApi() {
    return {
      v1SearchQueryGet: mockV1SearchQueryGet,
    };
  });
  const mockConfigurationConstructor = vi.fn(function Configuration() {});

  return {
    SearchApi: mockSearchApiConstructor,
    Configuration: mockConfigurationConstructor,
    ResponseCategory: {
      Product: 'product',
    },
    ResponseError: class ResponseError extends Error {
      response: any;

      constructor(message: string, response?: any) {
        super(message);
        this.response = response;
      }
    },
    __getMocks: () => ({
      mockV1SearchQueryGet,
      mockSearchApiConstructor,
      mockConfigurationConstructor,
    }),
  };
});

// FeatureFlagService
const testFeatureFlagService = new FeatureFlagService([]);
const testUserIdService = new UserIdentityService({ apiKey: '' });

// Mock useAppDetails
vi.mock('src/hooks/AppDetails/useAppDetails', () => ({
  useAppDetails: vi.fn(),
}));

// Mock useEnviveConfig
vi.mock('src/contexts/enviveConfigContext/enviveConfigContext', async () => {
  const actual = await vi.importActual('src/contexts/enviveConfigContext/enviveConfigContext');
  return {
    ...actual,
    useEnviveConfig: vi.fn(),
  };
});

// Mock Logger
vi.mock('src/application/logging/logger', () => {
  const MockLogger = vi.fn(function MockLogger() {
    return {
      logInfo: vi.fn(),
      logWarn: vi.fn(),
      logError: vi.fn(),
      logDebug: vi.fn(),
    };
  });

  return {
    default: MockLogger,
  };
});

// Mock the search service adapter
vi.mock('src/atoms/search/searchServiceAdapter', () => ({
  setSearchServiceFunction: vi.fn(),
  clearSearchServiceFunction: vi.fn(),
  getSearchServiceFunction: vi.fn(),
}));

// Get mocked functions
const getMocks = () => {
  return (commerceApiClient as any).__getMocks();
};

// Component that uses the useSearchService hook
const MockSearchComponent: React.FC = () => {
  const { searchProducts, isReady } = useSearchService();
  const [result, setResult] = React.useState<SearchResult | null>(null);
  const [error, setError] = React.useState<string | null>(null);
  const [loading, setLoading] = React.useState(false);

  const handleSearch = async () => {
    setLoading(true);
    setError(null);
    try {
      const searchResult = await searchProducts({ query: 'test query', limit: 10 });
      setResult(searchResult);
    } catch (err: any) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div data-testid="search-component">
      <div data-testid="is-ready">{isReady.toString()}</div>
      <div data-testid="loading">{loading.toString()}</div>
      <div data-testid="error">{error || 'null'}</div>
      {result && (
        <>
          <div data-testid="result-products-count">{result.products.length}</div>
          <div data-testid="result-total-count">{result.totalProductCount}</div>
          <div data-testid="result-response-id">{result.searchResponseId}</div>
        </>
      )}
      <button
        data-testid="search-btn"
        onClick={handleSearch}
        type="button"
      >
        Search
      </button>
    </div>
  );
};

// Component that reads baseUrlAtom
const BaseUrlReaderComponent: React.FC = () => {
  const baseUrl = useAtomValue(baseUrlAtom);
  return <div data-testid="base-url">{baseUrl || 'not-set'}</div>;
};

// Wrapper component to ensure providers are in correct order
const TestWrapper: React.FC<{
  children: React.ReactNode;
  orgLevelApiKey?: string;
  publicKey?: string;
  baseUrl?: string;
  appDetails?: {
    orgId?: string;
    userId?: string;
    orgShortName?: string;
    chatId?: string;
    source?: any;
    env?: any;
    variantInfo?: any;
  };
}> = ({
  children,
  orgLevelApiKey,
  publicKey,
  baseUrl = 'https://test-api.example.com',
  appDetails = {
    orgId: 'test-org-id',
    userId: 'test-user-id',
    orgShortName: 'test-org',
    chatId: 'test-chat-id',
    source: 'app',
    env: 'dev',
    variantInfo: {},
  },
}) => {
  // Mock useEnviveConfig
  (useEnviveConfig as any).mockReturnValue({
    orgLevelApiKey,
    publicKey,
  });

  // Mock useAppDetails
  (useAppDetails as any).mockReturnValue(appDetails);

  return (
    <Provider>
      <EnviveConfigProvider
        identifyingPrefix="test"
        orgShortName={appDetails?.orgShortName || 'test-org'}
        baseUrl={baseUrl}
        orgLevelApiKey={orgLevelApiKey}
        publicKey={publicKey}
      >
        <LocalStorageProvider>
          <GraphQLProvider>
            <UserIdentityProvider userIdService={testUserIdService}>
              <FeatureFlagServiceProvider featureFlagService={testFeatureFlagService}>
                <SearchProvider>{children}</SearchProvider>
              </FeatureFlagServiceProvider>
            </UserIdentityProvider>
          </GraphQLProvider>
        </LocalStorageProvider>
      </EnviveConfigProvider>
    </Provider>
  );
};

describe('SearchProvider', () => {
  let mocks: ReturnType<typeof getMocks>;

  beforeEach(() => {
    vi.clearAllMocks();
    mocks = getMocks();
    mocks.mockV1SearchQueryGet.mockReset();
    mocks.mockSearchApiConstructor.mockClear();
    mocks.mockConfigurationConstructor.mockClear();
    (setSearchServiceFunction as any).mockClear();
    (clearSearchServiceFunction as any).mockClear();
  });

  describe('useSearchService Hook Integration', () => {
    it('should provide context through useSearchService hook', async () => {
      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      expect(screen.getByTestId('search-component')).toBeInTheDocument();
    });

    it('should throw error when used outside of SearchProvider', () => {
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

      const TestComponent: React.FC = () => {
        try {
          useSearchService();
          return <div data-testid="no-error">No Error</div>;
        } catch (error: any) {
          return <div data-testid="error">{error.message}</div>;
        }
      };

      render(
        <Provider>
          <TestComponent />
        </Provider>,
      );

      expect(screen.getByTestId('error')).toHaveTextContent(
        'useSearchService must be used within a SearchProvider',
      );

      consoleSpy.mockRestore();
    });
  });

  describe('isReady State', () => {
    it('should be false when orgLevelApiKey is missing', async () => {
      render(
        <TestWrapper baseUrl="https://test-api.example.com">
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('false');
      });
    });

    it('should use default baseUrl when baseUrl is not provided', async () => {
      // baseUrlAtom has a default value, so even if baseUrl is empty, it will use the default
      // This test verifies that isReady works with the default baseUrl
      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl=""
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        // baseUrlAtom has a default, so isReady should be true if apiKey and appDetails are available
        const isReadyElement = screen.getByTestId('is-ready');
        expect(isReadyElement).toBeInTheDocument();
      });
    });

    it('should be false when appDetails is missing', async () => {
      (useAppDetails as any).mockReturnValue(null);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
          appDetails={null as any}
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('false');
      });
    });

    it('should be true when all dependencies are available', async () => {
      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });
    });

    it('should use publicKey when orgLevelApiKey is not available', async () => {
      render(
        <TestWrapper
          publicKey="public-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });
    });
  });

  describe('SearchApi Initialization', () => {
    it('should create SearchApi with correct configuration when ready', async () => {
      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      expect(mocks.mockConfigurationConstructor).toHaveBeenCalledWith(
        expect.objectContaining({
          accessToken: 'test-key',
          basePath: 'https://test-api.example.com',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
          },
        }),
      );
      expect(mocks.mockSearchApiConstructor).toHaveBeenCalled();
    });

    it('should not create SearchApi when not ready', async () => {
      render(
        <TestWrapper baseUrl="https://test-api.example.com">
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('false');
      });

      // SearchApi should not be created when not ready
      expect(mocks.mockSearchApiConstructor).not.toHaveBeenCalled();
    });
  });

  describe('searchProducts Function', () => {
    it('should search products successfully', async () => {
      const mockResponse = {
        products: [
          {
            id: 'product-1',
            response_id: 'resp-1',
            description: 'Test product',
            image_url: 'https://example.com/image.jpg',
            image_urls: ['https://example.com/image.jpg'],
            title: 'Test Product',
            url: 'https://example.com/product',
            original_price: 100,
            sale_price: 80,
            average_rating: 4.5,
            number_reviews: 10,
            metadata: {},
            is_for_grid: true,
            colors: [],
            sizes: [],
            filters: [],
          },
        ],
        filters: ['filter1', 'filter2'],
        search_response_id: 'search-resp-1',
      };

      mocks.mockV1SearchQueryGet.mockResolvedValueOnce(mockResponse);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('result-products-count')).toHaveTextContent('1');
      });

      expect(mocks.mockV1SearchQueryGet).toHaveBeenCalledWith({
        query: 'test query',
        limit: 10,
        org_id: 'test-org-id',
        user_id: 'test-user-id',
      });

      expect(screen.getByTestId('result-total-count')).toHaveTextContent('1');
      expect(screen.getByTestId('result-response-id')).toHaveTextContent('search-resp-1');
    });

    it('should transform product responses correctly', async () => {
      const mockResponse = {
        products: [
          {
            id: 'product-1',
            response_id: 'resp-1',
            description: 'Test product',
            image_url: 'https://example.com/image.jpg',
            image_urls: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
            title: 'Test Product',
            url: 'https://example.com/product',
            original_price: 100,
            sale_price: 80,
            average_rating: 4.5,
            number_reviews: 10,
            metadata: { key: 'value' },
            is_for_grid: true,
            colors: ['red', 'blue'],
            sizes: ['S', 'M', 'L'],
            filters: ['filter1'],
          },
        ],
        filters: ['filter1'],
        search_response_id: 'search-resp-1',
      };

      mocks.mockV1SearchQueryGet.mockResolvedValueOnce(mockResponse);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('result-products-count')).toHaveTextContent('1');
      });

      // Verify the transformed product has correct structure
      const searchResult = await screen.findByTestId('result-products-count');
      expect(searchResult).toBeInTheDocument();
    });

    it('should handle empty search results', async () => {
      const mockResponse = {
        products: [],
        filters: [],
        search_response_id: '',
      };

      mocks.mockV1SearchQueryGet.mockResolvedValueOnce(mockResponse);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('result-products-count')).toHaveTextContent('0');
      });

      expect(screen.getByTestId('result-total-count')).toHaveTextContent('0');
    });

    it('should throw error when not ready', async () => {
      render(
        <TestWrapper baseUrl="https://test-api.example.com">
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('false');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('error')).toHaveTextContent(
          'SearchService not ready - missing dependencies',
        );
      });
    });
  });

  describe('Error Handling', () => {
    it('should throw SessionRestartRequired for RESTART_SESSION error', async () => {
      const { ResponseError } = await import('@spiffy-ai/commerce-api-client');
      const mockError = new (ResponseError as any)('Session error', {
        json: async () => ({
          app_code: 'RESTART_SESSION',
        }),
      });

      mocks.mockV1SearchQueryGet.mockRejectedValueOnce(mockError);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('error')).toHaveTextContent('Session restart required');
      });
    });

    it('should throw SessionRestartRequired for NOT_FOUND sub_code', async () => {
      const { ResponseError } = await import('@spiffy-ai/commerce-api-client');
      const mockError = new (ResponseError as any)('Not found error', {
        json: async () => ({
          sub_code: 'NOT_FOUND',
        }),
      });

      mocks.mockV1SearchQueryGet.mockRejectedValueOnce(mockError);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('error')).toHaveTextContent('Session restart required');
      });
    });

    it('should throw UnsupportedProductException for unsupported product message', async () => {
      const { ResponseError } = await import('@spiffy-ai/commerce-api-client');
      const mockError = new (ResponseError as any)('Unsupported product', {
        json: async () => ({
          message: 'unsupported product',
        }),
      });

      mocks.mockV1SearchQueryGet.mockRejectedValueOnce(mockError);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('error')).toHaveTextContent('Unsupported product');
      });
    });

    it('should throw UnsupportedProductException for PRODUCT_NOT_FOUND app_code', async () => {
      const { ResponseError } = await import('@spiffy-ai/commerce-api-client');
      const mockError = new (ResponseError as any)('Product not found', {
        json: async () => ({
          app_code: 'PRODUCT_NOT_FOUND',
        }),
      });

      mocks.mockV1SearchQueryGet.mockRejectedValueOnce(mockError);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        expect(screen.getByTestId('error')).toHaveTextContent('Unsupported product');
      });
    });

    it('should rethrow non-ResponseError errors', async () => {
      const genericError = new Error('Generic error');
      mocks.mockV1SearchQueryGet.mockRejectedValueOnce(genericError);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      await waitFor(() => {
        const errorElement = screen.getByTestId('error');
        expect(errorElement).toBeInTheDocument();
        expect(errorElement.textContent).not.toBe('null');
      });
    });

    it('should handle ResponseError with invalid JSON response', async () => {
      const { ResponseError } = await import('@spiffy-ai/commerce-api-client');
      const mockError = new (ResponseError as any)('Error', {
        json: async () => {
          throw new Error('Invalid JSON');
        },
      });

      mocks.mockV1SearchQueryGet.mockRejectedValueOnce(mockError);

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await act(async () => {
        screen.getByTestId('search-btn').click();
      });

      // Should still throw the original error
      await waitFor(
        () => {
          const errorElement = screen.getByTestId('error');
          expect(errorElement).toBeInTheDocument();
          expect(errorElement.textContent).not.toBe('null');
        },
        { timeout: 2000 },
      );
    });
  });

  describe('Search Service Adapter Integration', () => {
    it('should register search function when ready', async () => {
      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      await waitFor(
        () => {
          expect(setSearchServiceFunction).toHaveBeenCalled();
        },
        { timeout: 2000 },
      );
    });

    it('should clear search function when not ready', async () => {
      render(
        <TestWrapper baseUrl="https://test-api.example.com">
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('false');
      });

      await waitFor(() => {
        expect(clearSearchServiceFunction).toHaveBeenCalled();
      });
    });

    it('should clear search function on unmount', async () => {
      const { unmount } = render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <MockSearchComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('is-ready')).toHaveTextContent('true');
      });

      unmount();

      await waitFor(() => {
        expect(clearSearchServiceFunction).toHaveBeenCalled();
      });
    });
  });

  describe('Context Value Memoization', () => {
    it('should maintain stable context value references', async () => {
      let contextValue1: any;
      let contextValue2: any;

      const Component1: React.FC = () => {
        const context = useSearchService();
        contextValue1 = context;
        return <div>Component1</div>;
      };

      const Component2: React.FC = () => {
        const context = useSearchService();
        contextValue2 = context;
        return <div>Component2</div>;
      };

      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://test-api.example.com"
        >
          <Component1 />
          <Component2 />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(contextValue1).toBeDefined();
        expect(contextValue2).toBeDefined();
        // Both components should receive the same context instance
        expect(contextValue1).toBe(contextValue2);
      });
    });
  });

  describe('BaseUrl Atom Integration', () => {
    it('should read baseUrl from atom', async () => {
      render(
        <TestWrapper
          orgLevelApiKey="test-key"
          baseUrl="https://custom-api.example.com"
        >
          <BaseUrlReaderComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('base-url')).toHaveTextContent('https://custom-api.example.com');
      });
    });
  });
});
