import React from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import { Provider, useStore } from 'jotai';
import {
  UserIdentityProvider,
  useUserIdentity,
} from 'src/contexts/userIdentityContext/userIdentityContext';
import { LocalStorageKeys, LocalStorageProvider } from 'src/contexts/localStorageContext';
import { EnviveConfigProvider } from 'src/contexts/enviveConfigContext/enviveConfigContext';
import {
  FeatureFlagService,
  FeatureFlagServiceProvider,
} from 'src/contexts/featureFlagServiceContext/featureFlagServiceContext';
import { userIdAtom } from 'src/atoms/app';
import { AmplitudeService } from 'src/services/amplitudeService/amplitudeService';
import { UserIdentityService } from 'src/services/userIdentityService';
import { AmplitudeProvider, SpiffyMetricsEventName, useAmplitude } from '../amplitudeContext';

// Mock AmplitudeService
const mockTrackEvent = vi.fn().mockResolvedValue(undefined);
const mockSetSupplementalDefaultProps = vi.fn();
const mockIsReady = vi.fn().mockReturnValue(true);
const mockIsMockApiKey = vi.fn().mockReturnValue(false);

vi.mock('src/services/amplitudeService/amplitudeService', async () => {
  const actual = await vi.importActual<
    typeof import('src/services/amplitudeService/amplitudeService')
  >('src/services/amplitudeService/amplitudeService');

  class MockAmplitudeService {
    trackEvent = mockTrackEvent;

    setSupplementalDefaultProps = mockSetSupplementalDefaultProps;

    get isReady(): boolean {
      return mockIsReady();
    }

    get isMockApiKey(): boolean {
      return mockIsMockApiKey();
    }
  }

  function MockAmplitudeServiceConstructor() {
    return new MockAmplitudeService();
  }
  MockAmplitudeServiceConstructor.prototype = MockAmplitudeService.prototype;
  const mockAmplitudeServiceConstructor = vi.fn(MockAmplitudeServiceConstructor);

  return {
    ...actual,
    AmplitudeService: mockAmplitudeServiceConstructor,
  };
});

// Component that uses the useAmplitude hook
const MockAmplitudeComponent: React.FC = () => {
  const amplitude = useAmplitude();

  return (
    <div data-testid="amplitude-component">
      <div data-testid="is-ready">{amplitude.isReady.toString()}</div>
      <div data-testid="is-mock-mode">{amplitude.isMockMode.toString()}</div>
      <button
        data-testid="track-event-button"
        type="button"
        onClick={() =>
          amplitude.trackEvent({
            eventName: SpiffyMetricsEventName.ChatUserMessageInput,
            eventProps: { message: 'test message' },
          })
        }
      >
        Track Event
      </button>
    </div>
  );
};

// Component that reads userIdAtom to verify it's set
const AtomReaderComponent: React.FC = () => {
  const store = useStore();
  const [userId, setUserId] = React.useState<string>('not-set');

  React.useEffect(() => {
    const unsubscribe = store.sub(userIdAtom, () => {
      try {
        const value = store.get(userIdAtom);
        if (value) {
          setUserId(value);
        }
      } catch {
        // Still not set
      }
    });

    const timer = setTimeout(() => {
      try {
        const value = store.get(userIdAtom);
        if (value) {
          setUserId(value);
        }
      } catch {
        // Not set yet
      }
    }, 50);

    return () => {
      unsubscribe();
      clearTimeout(timer);
    };
  }, [store]);

  return <div data-testid="atom-reader">{userId}</div>;
};

// Component that uses both contexts
const CombinedContextComponent: React.FC = () => {
  const userIdentity = useUserIdentity();
  const amplitude = useAmplitude();
  const [userId, setUserId] = React.useState<string>('');

  React.useEffect(() => {
    const fetchUserId = async () => {
      const id = await userIdentity.getUserIdOrDefault();
      setUserId(id);
    };

    fetchUserId();
  }, [userIdentity]);

  return (
    <div data-testid="combined-component">
      <div data-testid="user-id-from-identity">{userId}</div>
      <div data-testid="amplitude-ready">{amplitude.isReady.toString()}</div>
    </div>
  );
};

// FeatureFlagService
const testFeatureFlagService = new FeatureFlagService([]);

// userID service
const mockInitializeUser = vi.fn().mockResolvedValue(undefined);
const mockGetUserId = vi.fn().mockImplementation(async () => {
  // Check for override first
  const override = localStorage.getItem('v1-spiffy-user-id-override');
  if (override) return override;
  return 'test-user-id';
});

const testUserIdService = {
  getUserIdOrDefault: mockGetUserId,
  initializeUser: mockInitializeUser,
  getUserIdOverrideFromLocalStorage: vi.fn().mockImplementation(() => {
    return localStorage.getItem('v1-spiffy-user-id-override') || undefined;
  }),
} as unknown as UserIdentityService;

// Wrapper component with all required providers
const TestWrapper: React.FC<{
  children: React.ReactNode;
  amplitudeApiKey?: string;
  userIdOverride?: string;
  externalAmplitudeService?: AmplitudeService;
}> = ({
  children,
  amplitudeApiKey = 'test-amplitude-key',
  userIdOverride,
  externalAmplitudeService,
}) => {
  // Set up localStorage with user IDs if provided
  React.useEffect(() => {
    if (userIdOverride) {
      localStorage.setItem('v1-spiffy-user-id-override', userIdOverride);
    }
  }, [userIdOverride]);

  return (
    <Provider>
      <EnviveConfigProvider
        identifyingPrefix="test"
        orgShortName="test-org"
        amplitudeApiKey={amplitudeApiKey}
        dataResidency="US"
        env="test"
        contextSource="app"
        featureGates={[]}
      >
        <LocalStorageProvider>
          <UserIdentityProvider userIdService={testUserIdService}>
            <FeatureFlagServiceProvider featureFlagService={testFeatureFlagService}>
              <AmplitudeProvider externalAmplitudeService={externalAmplitudeService}>
                {children}
              </AmplitudeProvider>
            </FeatureFlagServiceProvider>
          </UserIdentityProvider>
        </LocalStorageProvider>
      </EnviveConfigProvider>
    </Provider>
  );
};

describe('AmplitudeProvider - React Context Integration', () => {
  let mockGetLocalStorageItem: (key: string) => string | null;
  beforeEach(() => {
    vi.clearAllMocks();
    vi.clearAllMocks();
    mockGetLocalStorageItem = vi.fn((key: string) => {
      if (key === LocalStorageKeys.ChatId) {
        return 'test-chat-id';
      }
      return null;
    });
    if (typeof localStorage !== 'undefined') {
      localStorage.clear();
    }
    // Reset mocks
    mockTrackEvent.mockClear();
    mockSetSupplementalDefaultProps.mockClear();
    mockIsReady.mockReturnValue(true);
    mockIsMockApiKey.mockReturnValue(false);
    if (AmplitudeService && typeof AmplitudeService === 'function') {
      (AmplitudeService as unknown as ReturnType<typeof vi.fn>).mockClear();
    }
  });

  describe('Integration with UserIdentityContext - userIdAtom', () => {
    it('should be ready when UserIdentityContext provides a userId', async () => {
      render(
        <TestWrapper>
          <CombinedContextComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        const isReady = screen.getByTestId('amplitude-ready').textContent;
        expect(isReady).toBe('true');
      });
    });

    it('should use userId from UserIdentityContext in tracking events', async () => {
      const testUserId = 'test-user-id-123';
      render(
        <TestWrapper userIdOverride={testUserId}>
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

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

      await act(async () => {
        screen.getByTestId('track-event-button').click();
      });

      await waitFor(() => {
        expect(mockTrackEvent).toHaveBeenCalled();
        const callArgs = mockTrackEvent.mock.calls[0];
        expect(callArgs[0]).toEqual({
          eventName: SpiffyMetricsEventName.ChatUserMessageInput,
          eventProps: { message: 'test message' },
        });
      });
    });

    it('should initialize AmplitudeService with userId from UserIdentityContext', async () => {
      const testUserId = 'init-user-id-345';
      render(
        <TestWrapper userIdOverride={testUserId}>
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(AmplitudeService).toHaveBeenCalled();
        const serviceCallArgs = (AmplitudeService as unknown as ReturnType<typeof vi.fn>).mock
          .calls[0];
        expect(serviceCallArgs[0]).toHaveProperty('userId', testUserId);
      });
    });
  });

  describe('userIdAtom synchronization', () => {
    it('should sync userIdAtom with UserIdentityContext userId', async () => {
      const testUserId = 'sync-user-id-678';
      render(
        <TestWrapper userIdOverride={testUserId}>
          <>
            <CombinedContextComponent />
            <AtomReaderComponent />
          </>
        </TestWrapper>,
      );

      await waitFor(() => {
        const identityUserId = screen.getByTestId('user-id-from-identity').textContent;
        const atomUserId = screen.getByTestId('atom-reader').textContent;
        expect(identityUserId).toBe(testUserId);
        expect(atomUserId).toBe(testUserId);
        expect(identityUserId).toBe(atomUserId);
      });
    });
  });

  describe('Context readiness and initialization', () => {
    it('should wait for UserIdentityContext to be ready before becoming ready', async () => {
      render(
        <TestWrapper>
          <CombinedContextComponent />
        </TestWrapper>,
      );

      // Both contexts should eventually be ready
      await waitFor(() => {
        const amplitudeReady = screen.getByTestId('amplitude-ready').textContent;
        expect(amplitudeReady).toBe('true');
      });
    });

    it('should initialize AmplitudeService with correct config from UserIdentityContext', async () => {
      const testUserId = 'init-client-user-id';
      render(
        <TestWrapper userIdOverride={testUserId}>
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(AmplitudeService).toHaveBeenCalled();
        const serviceCallArgs = (AmplitudeService as unknown as ReturnType<typeof vi.fn>).mock
          .calls[0];
        expect(serviceCallArgs[0]).toMatchObject({
          userId: testUserId,
          amplitudeApiKey: 'test-amplitude-key',
          dataResidency: 'US',
        });
      });
    });

    it('should render children with isReady=false when isMockMode=true', async () => {
      mockIsReady.mockReturnValue(false);
      mockIsMockApiKey.mockReturnValue(true);

      render(
        <TestWrapper amplitudeApiKey="mock-amplitude-key">
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('amplitude-component')).toBeInTheDocument();
        expect(screen.getByTestId('is-ready').textContent).toBe('false');
        expect(screen.getByTestId('is-mock-mode').textContent).toBe('true');
      });
    });

    it('should NOT render children when isReady=false and isMockMode=false', async () => {
      mockIsReady.mockReturnValue(false);
      mockIsMockApiKey.mockReturnValue(false);

      render(
        <TestWrapper>
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.queryByTestId('amplitude-component')).not.toBeInTheDocument();
      });
    });
  });

  describe('External service injection', () => {
    it('should use external AmplitudeService when provided', async () => {
      const externalService = new AmplitudeService({
        userId: 'external-user-id',
        amplitudeApiKey: 'external-api-key',
        dataResidency: 'EU',
        orgId: 'test-org-id',
        show: true,
        enviveOn: true,
        getLocalStorageItem: mockGetLocalStorageItem,
        featureFlagService: new FeatureFlagService([]),
        env: 'test', // Provide required fields to match AmplitudeServiceConfig
        contextSource: 'test-source',
        orgShortName: 'test-org',
      });

      render(
        <TestWrapper externalAmplitudeService={externalService}>
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

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

      await act(async () => {
        screen.getByTestId('track-event-button').click();
      });

      await waitFor(() => {
        expect(mockTrackEvent).toHaveBeenCalled();
        const callArgs = mockTrackEvent.mock.calls[0];
        expect(callArgs[0]).toEqual({
          eventName: SpiffyMetricsEventName.ChatUserMessageInput,
          eventProps: { message: 'test message' },
        });
      });
    });

    it('should not create new service when external service is provided', async () => {
      // Provide all required fields to match AmplitudeServiceConfig
      const externalService = new AmplitudeService({
        userId: 'external-user-id',
        amplitudeApiKey: 'external-api-key',
        dataResidency: 'EU',
        orgId: 'test-org-id',
        show: true,
        enviveOn: true,
        getLocalStorageItem: mockGetLocalStorageItem,
        featureFlagService: new FeatureFlagService([]),
        env: 'test',
        contextSource: 'test-source',
        orgShortName: 'test-org',
      });

      const serviceCallCountBefore = (AmplitudeService as unknown as ReturnType<typeof vi.fn>).mock
        .calls.length;

      render(
        <TestWrapper externalAmplitudeService={externalService}>
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

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

      // Should not have created an additional service instance
      // The provider should use the external service directly
      const serviceCallCountAfter = (AmplitudeService as unknown as ReturnType<typeof vi.fn>).mock
        .calls.length;
      expect(serviceCallCountAfter).toBe(serviceCallCountBefore);
    });
  });

  describe('useAmplitude hook', () => {
    it('should throw error when used outside provider', () => {
      // Suppress console.error for this test
      const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});

      expect(() => {
        render(<MockAmplitudeComponent />);
      }).toThrow('useAmplitude must be used within AmplitudeProvider');

      consoleError.mockRestore();
    });

    it('should provide amplitude context when used within provider', async () => {
      render(
        <TestWrapper>
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

      await waitFor(() => {
        expect(screen.getByTestId('amplitude-component')).toBeInTheDocument();
        expect(screen.getByTestId('is-ready')).toBeInTheDocument();
      });
    });
  });

  describe('Service instance management', () => {
    it('should update service when dependencies change', async () => {
      const { rerender } = render(
        <TestWrapper amplitudeApiKey="initial-key">
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

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

      const initialServiceCalls = (AmplitudeService as unknown as ReturnType<typeof vi.fn>).mock
        .calls.length;

      rerender(
        <TestWrapper amplitudeApiKey="updated-key">
          <MockAmplitudeComponent />
        </TestWrapper>,
      );

      // Service should be recreated with new config
      await waitFor(() => {
        const serviceCallCount = (AmplitudeService as unknown as ReturnType<typeof vi.fn>).mock
          .calls.length;
        expect(serviceCallCount).toBeGreaterThan(initialServiceCalls);
      });
    });
  });
});
