import React from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { Provider, useAtomValue } from 'jotai';
import { EnviveConfigProvider } from 'src/contexts/enviveConfigContext/enviveConfigContext';
import { baseUrlAtom, orgLevelApiKeyAtom } from 'src/atoms/envive/enviveConfig';
import Logger from 'src/application/logging/logger';
import { getMerchantOrgIdQuery } from 'src/application/models/graphql/queries/getMerchantOrgIdQuery';
import { GraphQLProvider, useGraphQLClient } from '../graphqlContext';

// Mock the Logger to avoid console output in tests
vi.spyOn(Logger.prototype, 'logDebug').mockImplementation(() => {});
vi.spyOn(Logger.prototype, 'logWarn').mockImplementation(() => {});
vi.spyOn(Logger.prototype, 'logError').mockImplementation(() => {});

// Mock fetch globally
global.fetch = vi.fn();

// Component that uses the useGraphQLClient hook
const MockGraphQLComponent: React.FC = () => {
  const client = useGraphQLClient();

  return (
    <div data-testid="graphql-component">
      <div data-testid="is-ready">{client.isReady.toString()}</div>
      <button
        data-testid="execute-query-btn"
        type="button"
        onClick={async () => {
          try {
            const result = await client.executeQuery('query { test }');
            (window as any).lastQueryResult = result;
          } catch (error) {
            (window as any).lastQueryError = error;
          }
        }}
      >
        Execute Query
      </button>
      <button
        data-testid="get-org-id-btn"
        type="button"
        onClick={async () => {
          try {
            const orgId = await client.getOrgId();
            (window as any).lastOrgId = orgId;
          } catch (error) {
            (window as any).lastOrgIdError = error;
          }
        }}
      >
        Get Org ID
      </button>
      <button
        data-testid="get-config-btn"
        type="button"
        onClick={async () => {
          try {
            const config = await client.getColorsAndFrontendConfig();
            (window as any).lastConfig = config;
          } catch (error) {
            (window as any).lastConfigError = error;
          }
        }}
      >
        Get Config
      </button>
    </div>
  );
};

// Component that reads atoms directly to verify they're set
const AtomReaderComponent: React.FC = () => {
  const apiKey = useAtomValue(orgLevelApiKeyAtom);
  const baseUrl = useAtomValue(baseUrlAtom);

  return (
    <div data-testid="atom-reader">
      <div data-testid="atom-api-key">{apiKey || 'not-set'}</div>
      <div data-testid="atom-base-url">{baseUrl || 'not-set'}</div>
    </div>
  );
};

// Wrapper component to ensure providers are in correct order
const TestWrapper: React.FC<{
  children: React.ReactNode;
  orgLevelApiKey?: string;
  baseUrl?: string;
}> = ({ children, orgLevelApiKey, baseUrl }) => {
  return (
    <Provider>
      <EnviveConfigProvider
        identifyingPrefix="test"
        orgLevelApiKey={orgLevelApiKey}
        baseUrl={baseUrl}
      >
        <GraphQLProvider>{children}</GraphQLProvider>
      </EnviveConfigProvider>
    </Provider>
  );
};

describe('GraphQLProvider', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    (global.fetch as any).mockClear();
    // Clear any stored results
    delete (window as any).lastQueryResult;
    delete (window as any).lastQueryError;
    delete (window as any).lastOrgId;
    delete (window as any).lastOrgIdError;
    delete (window as any).lastConfig;
    delete (window as any).lastConfigError;
  });

  describe('Provider Integration with Jotai Store', () => {
    it('should read atoms correctly when provider is mounted', async () => {
      const testConfig = {
        orgLevelApiKey: 'test-api-key',
        baseUrl: 'https://test-api.example.com',
      };

      render(
        <TestWrapper {...testConfig}>
          <AtomReaderComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('atom-api-key')).toHaveTextContent(testConfig.orgLevelApiKey);
      });

      expect(screen.getByTestId('atom-base-url')).toHaveTextContent(testConfig.baseUrl);
    });
  });

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

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

      // Verify all context methods are accessible
      expect(screen.getByTestId('graphql-component')).toBeInTheDocument();
    });

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

      const TestComponent: React.FC = () => {
        try {
          useGraphQLClient();
          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(
        'useGraphQLClient must be used within a GraphQLProvider',
      );

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

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

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

    it('should be true when baseUrl is missing but has default value', async () => {
      // Note: baseUrlAtom has a default value, so isReady will be true
      // even if baseUrl is not explicitly provided
      render(
        <TestWrapper orgLevelApiKey="test-key">
          <MockGraphQLComponent />
        </TestWrapper>,
      );

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

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

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

  describe('executeQuery', () => {
    it('should execute a GraphQL query successfully', async () => {
      const mockResponse = {
        data: { test: 'result' },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

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

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

      await waitFor(() => {
        expect((window as any).lastQueryResult).toEqual(mockResponse.data);
      });

      expect(global.fetch).toHaveBeenCalledWith('https://test-api.example.com/v1/graphql', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: 'Bearer test-key',
        },
        body: JSON.stringify({ query: 'query { test }', variables: undefined }),
      });
    });

    it('should execute a query with variables', async () => {
      const mockResponse = {
        data: { result: 'success' },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const TestComponent: React.FC = () => {
        const client = useGraphQLClient();
        React.useEffect(() => {
          if (client.isReady) {
            client.executeQuery('query Test($id: String)', { id: '123' }).then(result => {
              (window as any).lastQueryResult = result;
            });
          }
        }, [client]);

        return <div>Test</div>;
      };

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

      await waitFor(() => {
        expect((window as any).lastQueryResult).toEqual(mockResponse.data);
      });

      expect(global.fetch).toHaveBeenCalledWith('https://test-api.example.com/v1/graphql', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: 'Bearer test-key',
        },
        body: JSON.stringify({
          query: 'query Test($id: String)',
          variables: { id: '123' },
        }),
      });
    });

    it('should throw error when client is not ready', async () => {
      const TestComponent: React.FC = () => {
        const client = useGraphQLClient();
        React.useEffect(() => {
          client.executeQuery('query { test }').catch(error => {
            (window as any).lastQueryError = error;
          });
        }, [client]);

        return <div>Test</div>;
      };

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

      await waitFor(() => {
        expect((window as any).lastQueryError).toBeDefined();
        expect((window as any).lastQueryError.message).toBe(
          'GraphQL client not ready - missing apiKey or baseUrl',
        );
      });
    });

    it('should throw error when fetch fails', async () => {
      (global.fetch as any).mockResolvedValueOnce({
        ok: false,
        statusText: 'Internal Server Error',
      });

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

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

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

      await waitFor(() => {
        expect((window as any).lastQueryError).toBeDefined();
        expect((window as any).lastQueryError.message).toContain('GraphQL request failed');
      });
    });

    it('should throw error when GraphQL response contains errors', async () => {
      const mockResponse = {
        data: null,
        errors: [{ message: 'GraphQL error' }],
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

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

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

      await waitFor(() => {
        expect((window as any).lastQueryError).toBeDefined();
        expect((window as any).lastQueryError.message).toContain('GraphQL errors');
      });
    });
  });

  describe('getOrgId', () => {
    it('should fetch and return org ID successfully', async () => {
      const mockOrgId = 'org-123';
      const mockResponse = {
        data: {
          me: {
            org: {
              id: mockOrgId,
            },
          },
        },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

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

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

      await waitFor(() => {
        expect((window as any).lastOrgId).toBe(mockOrgId);
      });

      expect(global.fetch).toHaveBeenCalledWith(
        'https://test-api.example.com/v1/graphql',
        expect.objectContaining({
          body: JSON.stringify({
            query: getMerchantOrgIdQuery,
            variables: undefined,
          }),
        }),
      );
    });

    it('should return undefined when org ID is not available', async () => {
      const mockResponse = {
        data: {
          me: {
            org: null,
          },
        },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

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

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

      await waitFor(() => {
        expect((window as any).lastOrgId).toBeUndefined();
      });
    });

    it('should return undefined when org.id is empty string', async () => {
      const mockResponse = {
        data: {
          me: {
            org: {
              id: '',
            },
          },
        },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

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

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

      await waitFor(() => {
        expect((window as any).lastOrgId).toBeUndefined();
      });
    });
  });

  describe('getColorsAndFrontendConfig', () => {
    it('should fetch and return colors and frontend config successfully', async () => {
      const mockColorsConfig = {
        accent_primary: '#FF0000',
        accent_secondary: '#00FF00',
        background_dark: '#000000',
      };
      const mockFrontendConfig = {
        merchant_override_css: 'test-css',
        page_variants: [],
      };

      const mockResponse = {
        data: {
          me: {
            getProductsConfigByVersion: {
              colors: {
                values: mockColorsConfig,
              },
              frontend: {
                values: mockFrontendConfig,
              },
            },
          },
        },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

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

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

      await waitFor(() => {
        const config = (window as any).lastConfig;
        expect(config).toBeDefined();
        expect(config.colorsConfig).toBeDefined();
        expect(config.frontendConfig).toBeDefined();
      });
    });

    it('should handle missing colors and frontend config gracefully', async () => {
      const mockResponse = {
        data: {
          me: {
            getProductsConfigByVersion: null,
          },
        },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

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

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

      await waitFor(() => {
        const config = (window as any).lastConfig;
        expect(config).toBeDefined();
        expect(config.colorsConfig).toBeUndefined();
        expect(config.frontendConfig).toBeUndefined();
      });
    });

    it('should handle errors and return undefined configs', async () => {
      const logErrorSpy = vi.spyOn(Logger.prototype, 'logError');

      (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));

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

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

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

      await waitFor(() => {
        const config = (window as any).lastConfig;
        expect(config).toBeDefined();
        expect(config.colorsConfig).toBeUndefined();
        expect(config.frontendConfig).toBeUndefined();
        expect(logErrorSpy).toHaveBeenCalledWith(
          'Error fetching graphql colors and frontend config',
          expect.any(Error),
        );
      });
    });

    it('should transform snake_case to camelCase', async () => {
      const mockColorsConfig = {
        accent_primary: '#FF0000',
        accent_secondary: '#00FF00',
      };
      const mockFrontendConfig = {
        merchant_override_css: 'test-css',
        page_variants: [],
      };

      const mockResponse = {
        data: {
          me: {
            getProductsConfigByVersion: {
              colors: {
                values: mockColorsConfig,
              },
              frontend: {
                values: mockFrontendConfig,
              },
            },
          },
        },
      };

      (global.fetch as any).mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

      const TestComponent: React.FC = () => {
        const client = useGraphQLClient();
        const [config, setConfig] = React.useState<any>(null);

        React.useEffect(() => {
          if (client.isReady) {
            client.getColorsAndFrontendConfig().then(setConfig);
          }
        }, [client]);

        return (
          <div>
            {config && (
              <div data-testid="config-display">
                <div data-testid="has-colors">{config.colorsConfig ? 'yes' : 'no'}</div>
                <div data-testid="has-frontend">{config.frontendConfig ? 'yes' : 'no'}</div>
              </div>
            )}
          </div>
        );
      };

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

      await waitFor(() => {
        expect(screen.getByTestId('has-colors')).toHaveTextContent('yes');
        expect(screen.getByTestId('has-frontend')).toHaveTextContent('yes');
      });
    });
  });

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

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

      const Component2: React.FC = () => {
        const context = useGraphQLClient();
        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);
      });
    });
  });
});
