import { FeatureFlagService } from 'src/contexts/featureFlagServiceContext/featureFlagServiceContext';
import { FeatureGates } from 'src/application/models/featureGates';
import { LocalStorageKeys } from 'src/contexts/localStorageContext';
import Logger from 'src/application/logging/logger';
import { SpiffyEvent } from 'src/events';
import {
  AmplitudeService,
  AmplitudeServiceConfig,
  SpiffyMetricsEventName,
} from '../amplitudeService';

const { mockTrack, mockInit, mockAdd, mockDispatch } = vi.hoisted(() => {
  return {
    // Mock Amplitude
    mockTrack: vi.fn(),
    mockInit: vi.fn(),
    mockAdd: vi.fn(),
    // Mock EventsDispatcher
    mockDispatch: vi.fn(),
  };
});

vi.mock('@amplitude/analytics-browser', () => ({
  createInstance: vi.fn(() => ({
    track: mockTrack,
    init: mockInit,
    add: mockAdd,
  })),
}));

vi.mock('src/events', () => ({
  EventsDispatcher: {
    dispatch: mockDispatch,
  },
  SpiffyEvent: {
    AMPLITUDE_EVENT: 'AMPLITUDE_EVENT',
  },
}));

// Mock crypto.subtle.digest for insert_id generation
const mockDigest = vi.fn().mockResolvedValue(new Uint8Array(32).fill(0));
Object.defineProperty(global, 'crypto', {
  value: {
    subtle: {
      digest: mockDigest,
    },
  },
  writable: true,
});

describe('AmplitudeService', () => {
  let featureFlagService: FeatureFlagService;
  let mockGetLocalStorageItem: (key: string) => string | null;

  beforeEach(() => {
    vi.clearAllMocks();
    mockGetLocalStorageItem = vi.fn((key: string) => {
      if (key === LocalStorageKeys.ChatId) {
        return 'test-chat-id';
      }
      return null;
    });
    featureFlagService = new FeatureFlagService([]);
    mockTrack.mockClear();
    mockInit.mockClear();
    mockAdd.mockClear();
    mockDigest.mockResolvedValue(new Uint8Array(32).fill(0));
    mockDispatch.mockClear();

    // Mock window.location for tests that need it
    Object.defineProperty(global, 'window', {
      value: {
        location: {
          href: 'https://test.example.com',
        },
        localStorage: {
          getItem: vi.fn(),
        },
        performance: {
          now: vi.fn(() => Date.now()),
        },
      },
      writable: true,
      configurable: true,
    });
  });

  const validApiKey = 'abcdef1234567890abcdef1234567890';

  const createService = (overrides?: Partial<AmplitudeServiceConfig>) => {
    return new AmplitudeService({
      userId: 'test-user-id',
      amplitudeApiKey: validApiKey,
      dataResidency: 'US',
      env: 'test',
      orgId: 'test-org-id',
      show: true,
      enviveOn: true,
      contextSource: 'app',
      orgShortName: 'test-org',
      featureFlagService,
      getLocalStorageItem: mockGetLocalStorageItem,
      ...overrides,
    });
  };

  describe('Initialization', () => {
    it('should initialize Amplitude client when all required config is provided', () => {
      createService();

      expect(mockInit).toHaveBeenCalledWith(
        validApiKey,
        'test-user-id',
        expect.objectContaining({
          serverZone: 'US',
          trackingOptions: {
            ipAddress: true,
          },
        }),
      );
    });

    it('should not be ready if userId is missing', () => {
      const service = new AmplitudeService({
        userId: '',
        amplitudeApiKey: validApiKey,
        dataResidency: 'US',
        env: 'test',
        orgId: 'test-org-id',
        show: true,
        enviveOn: true,
        getLocalStorageItem: mockGetLocalStorageItem,
        contextSource: 'app',
        orgShortName: 'test-org',
        featureFlagService,
      });

      expect(service.isReady).toBe(false);
    });

    it('should not be ready if amplitudeApiKey is missing', () => {
      const service = new AmplitudeService({
        userId: 'test-user-id',
        amplitudeApiKey: '',
        dataResidency: 'US',
        env: 'test',
        orgId: 'test-org-id',
        show: true,
        enviveOn: true,
        getLocalStorageItem: mockGetLocalStorageItem,
        contextSource: 'app',
        orgShortName: 'test-org',
        featureFlagService,
      });

      expect(service.isReady).toBe(false);
    });

    it('should not be ready if featureFlagService is missing', () => {
      const service = new AmplitudeService({
        userId: 'test-user-id',
        amplitudeApiKey: validApiKey,
        dataResidency: 'US',
        env: 'test',
        orgId: 'test-org-id',
        show: true,
        enviveOn: true,
        getLocalStorageItem: mockGetLocalStorageItem,
        contextSource: 'app',
        orgShortName: 'test-org',
        featureFlagService: null as any,
      });

      expect(service.isReady).toBe(false);
    });

    it('should be ready when all required config is provided', () => {
      const service = createService();

      expect(service.isReady).toBe(true);
    });
  });

  describe('trackEvent', () => {
    it('should track events with correct event name format', async () => {
      const service = createService();

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
      });

      expect(mockTrack).toHaveBeenCalledWith(
        '[Spiffy] Chat User Message Input',
        expect.objectContaining({
          message: 'test',
        }),
        expect.objectContaining({
          insert_id: expect.any(String),
        }),
      );
    });

    it('should include default tracking props in events', async () => {
      const service = createService({
        env: 'test',
        contextSource: 'app',
        orgShortName: 'test-org',
      });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
      });

      const callArgs = mockTrack.mock.calls[0];
      const eventProps = callArgs[1];

      expect(eventProps).toHaveProperty('user.id', 'test-user-id');
      expect(eventProps).toHaveProperty('env', 'test');
      expect(eventProps).toHaveProperty('app_source', 'app');
      expect(eventProps).toHaveProperty('org.short_name', 'test-org');
      expect(eventProps).toHaveProperty('chat_id', 'test-chat-id');
      expect(eventProps).toHaveProperty('app_id', 'commerce-chat-react-component');
    });

    it('should prefix event props with event name', async () => {
      const service = createService();

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test', count: 5 },
      });

      expect(mockTrack).toHaveBeenCalled();
      const callArgs = mockTrack.mock.calls[0];
      const eventProps = callArgs[1];

      expect(eventProps).toHaveProperty('chat_user_message_input.message', 'test');
      expect(eventProps).toHaveProperty('chat_user_message_input.count', 5);
    });

    it('should generate insert_id from event data', async () => {
      const service = createService();

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
      });

      expect(mockDigest).toHaveBeenCalled();
      const callArgs = mockTrack.mock.calls[0];
      expect(callArgs[2]).toHaveProperty('insert_id');
    });

    it('should include eventGroups in track call', async () => {
      const service = createService();

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
        eventGroups: { group1: 'value1', group2: 'value2' },
      });

      expect(mockTrack).toHaveBeenCalled();
      const callArgs = mockTrack.mock.calls[0];
      expect(callArgs[2]).toHaveProperty('group1', 'value1');
      expect(callArgs[2]).toHaveProperty('group2', 'value2');
    });

    it('should not track if client is not initialized', async () => {
      const service = new AmplitudeService({
        userId: '',
        amplitudeApiKey: validApiKey,
        dataResidency: 'US',
        env: 'test',
        orgId: 'test-org-id',
        show: true,
        enviveOn: true,
        getLocalStorageItem: mockGetLocalStorageItem,
        contextSource: 'app',
        orgShortName: 'test-org',
        featureFlagService,
      });

      const logWarnSpy = vi.spyOn(Logger.prototype, 'logWarn');

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
      });

      expect(mockTrack).not.toHaveBeenCalled();
      expect(logWarnSpy).toHaveBeenCalledWith(
        'amplitude client undefined',
        undefined,
        expect.objectContaining({
          event_name: '[Spiffy] Chat User Message Input',
        }),
      );
    });

    it('should handle tracking errors gracefully', async () => {
      const service = createService();
      const error = new Error('Tracking error');
      mockTrack.mockImplementationOnce(() => {
        throw error;
      });

      const logErrorSpy = vi.spyOn(Logger.prototype, 'logError');

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
      });

      expect(logErrorSpy).toHaveBeenCalledWith(
        'Error tracking event',
        error,
        expect.objectContaining({
          eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        }),
      );
    });

    it('should send to Google Analytics when alsoSendToGoogleAnalytics is true', async () => {
      const service = createService({
        orgGaConfig: { enabled: true },
      });

      const dataLayer: any[] = [];
      Object.defineProperty(global, 'window', {
        value: {
          location: {
            href: 'https://test.example.com',
          },
          localStorage: {
            getItem: vi.fn(),
          },
          performance: {
            now: vi.fn(() => Date.now()),
          },
          dataLayer,
        },
        writable: true,
        configurable: true,
      });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
        alsoSendToGoogleAnalytics: true,
      });

      expect(dataLayer).toHaveLength(1);
      expect(dataLayer[0]).toEqual({
        event: '[Spiffy] Chat User Message Input',
        eventProps: { message: 'test' },
      });
    });

    it('should not send to Google Analytics when orgGaConfig is not provided', async () => {
      const service = createService();

      const dataLayer: any[] = [];
      Object.defineProperty(global, 'window', {
        value: {
          dataLayer,
        },
        writable: true,
        configurable: true,
      });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
        alsoSendToGoogleAnalytics: true,
      });

      expect(dataLayer).toHaveLength(0);
    });
  });

  describe('Default tracking props', () => {
    it('should include feature gates in default props', async () => {
      const featureGates = [
        { name: FeatureGates.IsNewFeatureEnabled, value: true },
        { name: FeatureGates.IsClientSessionEnabled, value: false },
      ];
      const featureFlagServiceWithGates = new FeatureFlagService(featureGates);
      const service = createService({ featureFlagService: featureFlagServiceWithGates });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
      });

      const callArgs = mockTrack.mock.calls[0];
      const eventProps = callArgs[1];

      expect(eventProps).toHaveProperty('feature_gate.is_new_feature_enabled', true);
      expect(eventProps).toHaveProperty('feature_gate.is_client_session_enabled', false);
    });

    it('should use localStorage getItem when provided', async () => {
      const customGetItem = vi.fn((key: string) => {
        if (key === LocalStorageKeys.ChatId) {
          return 'custom-chat-id';
        }
        return null;
      });
      const service = createService({ getLocalStorageItem: customGetItem });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
      });

      expect(customGetItem).toHaveBeenCalledWith(LocalStorageKeys.ChatId);
      const callArgs = mockTrack.mock.calls[0];
      expect(callArgs[1]).toHaveProperty('chat_id', 'custom-chat-id');
    });

    it('should fall back to window.localStorage when getLocalStorageItem is not provided', async () => {
      const service = new AmplitudeService({
        userId: 'test-user-id',
        amplitudeApiKey: validApiKey,
        dataResidency: 'US',
        env: 'test',
        orgId: 'test-org-id',
        show: true,
        enviveOn: true,
        getLocalStorageItem: null,
        contextSource: 'app',
        orgShortName: 'test-org',
        featureFlagService,
      });

      // Mock window.localStorage
      const mockLocalStorage = {
        getItem: vi.fn((key: string) => {
          if (key === LocalStorageKeys.ChatId) {
            return 'window-chat-id';
          }
          return null;
        }),
      };
      Object.defineProperty(global, 'window', {
        value: {
          location: {
            href: 'https://test.example.com',
          },
          localStorage: mockLocalStorage,
          performance: {
            now: vi.fn(() => Date.now()),
          },
        },
        writable: true,
        configurable: true,
      });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
      });

      // Check that getItem was called with ChatId (may be called with other keys too)
      expect(mockLocalStorage.getItem).toHaveBeenCalledWith(LocalStorageKeys.ChatId);
      expect(mockTrack).toHaveBeenCalled();
      const callArgs = mockTrack.mock.calls[0];
      expect(callArgs[1]).toHaveProperty('chat_id', 'window-chat-id');
    });

    it('should include supplemental default props', async () => {
      const service = createService();
      service.setSupplementalDefaultProps({ customProp: 'customValue', anotherProp: 123 });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
      });

      const callArgs = mockTrack.mock.calls[0];
      const eventProps = callArgs[1];

      expect(eventProps).toHaveProperty('customProp', 'customValue');
      expect(eventProps).toHaveProperty('anotherProp', 123);
    });
  });

  describe('setSupplementalDefaultProps', () => {
    it('should set supplemental default props', async () => {
      const service = createService();
      service.setSupplementalDefaultProps({ test: 'value' });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
      });

      expect(mockTrack).toHaveBeenCalled();
      const callArgs = mockTrack.mock.calls[0];
      const eventProps = callArgs[1];
      expect(eventProps).toHaveProperty('test', 'value');
    });
  });

  describe('Event enrichment', () => {
    it('should enrich Page Viewed events', async () => {
      // Ensure window.location is properly mocked
      Object.defineProperty(global, 'window', {
        value: {
          location: {
            href: 'https://test.example.com',
          },
          localStorage: {
            getItem: vi.fn(),
          },
          performance: {
            now: vi.fn(() => Date.now()),
          },
        },
        writable: true,
        configurable: true,
      });

      createService({
        show: true,
      });

      // Simulate a Page Viewed event by calling the enrichment plugin
      // The enrichment is added during initialization
      expect(mockAdd).toHaveBeenCalled();

      // Get the enrichment plugin that was added
      const enrichmentCall = mockAdd.mock.calls[0];
      const enrichment = enrichmentCall[0];

      const mockEvent = {
        event_type: '[Amplitude] Page Viewed',
        event_properties: {},
      };

      await enrichment.execute(mockEvent as any);

      expect(mockDispatch).toHaveBeenCalledWith(
        SpiffyEvent.AMPLITUDE_EVENT,
        expect.objectContaining({
          event_type: '[Amplitude] Page Viewed',
          event_properties: expect.objectContaining({
            'globalProperties.show': 'true',
          }),
        }),
      );
    });

    it('should enrich Bundle Loaded events', async () => {
      // Ensure window.location is properly mocked
      Object.defineProperty(global, 'window', {
        value: {
          location: {
            href: 'https://test.example.com',
          },
          localStorage: {
            getItem: vi.fn(),
          },
          performance: {
            now: vi.fn(() => Date.now()),
          },
        },
        writable: true,
        configurable: true,
      });

      createService();

      const enrichmentCall = mockAdd.mock.calls[0];
      const enrichment = enrichmentCall[0];

      const mockEvent = {
        event_type: `[Spiffy] ${SpiffyMetricsEventName.BundleLoaded}`,
        event_properties: {},
      };

      await enrichment.execute(mockEvent as any);

      expect(mockDispatch).toHaveBeenCalled();
    });

    it('should not enrich non-specified events', async () => {
      createService();

      const enrichmentCall = mockAdd.mock.calls[0];
      const enrichment = enrichmentCall[0];

      const mockEvent = {
        event_type: '[Spiffy] Some Other Event',
        event_properties: {},
      };

      const result = await enrichment.execute(mockEvent as any);

      expect(result).toEqual(mockEvent);
      expect(mockDispatch).toHaveBeenCalledWith(SpiffyEvent.AMPLITUDE_EVENT, mockEvent);
    });
  });

  describe('isMockApiKey', () => {
    it('should return true when amplitudeApiKey is "mock-amplitude-key"', () => {
      const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });

      expect(service.isMockApiKey).toBe(true);
    });

    it('should return false for a real API key', () => {
      const service = createService();

      expect(service.isMockApiKey).toBe(false);
    });
  });

  describe('Mock API key behavior', () => {
    it('should not initialize the Amplitude client when using mock API key', () => {
      createService({ amplitudeApiKey: 'mock-amplitude-key' });

      expect(mockInit).not.toHaveBeenCalled();
      expect(mockAdd).not.toHaveBeenCalled();
    });

    it('should log a warning when mock API key is detected during initialization', () => {
      const logWarnSpy = vi.spyOn(Logger.prototype, 'logWarn');

      createService({ amplitudeApiKey: 'mock-amplitude-key' });

      expect(logWarnSpy).toHaveBeenCalledWith(
        'Mock API key detected — running in mock mode, no events will be sent to Amplitude.',
        'mock-amplitude-key',
      );
    });

    it('should report isReady as false when using mock API key', () => {
      const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });

      expect(service.isReady).toBe(false);
    });

    it('should not track events when using mock API key', async () => {
      const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { message: 'test' },
      });

      expect(mockTrack).not.toHaveBeenCalled();
    });

    it('should not call crypto.subtle.digest when using mock API key', async () => {
      const service = createService({ amplitudeApiKey: 'mock-amplitude-key' });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
      });

      expect(mockDigest).not.toHaveBeenCalled();
    });
  });

  describe('Session replay initialization', () => {
    it('should disable session replay by default', () => {
      createService();

      // Session replay is always disabled in the current implementation
      expect(mockInit).toHaveBeenCalledWith(
        expect.any(String),
        expect.any(String),
        expect.objectContaining({
          autocapture: expect.objectContaining({
            sessions: false,
          }),
        }),
      );
    });
  });

  describe('Multiple events', () => {
    it('should track multiple events with correct userId', async () => {
      const service = createService();

      // Ensure the service is ready
      expect(service.isReady).toBe(true);

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatUserMessageInput,
        eventProps: { event1: true },
      });

      await service.trackEvent({
        eventName: SpiffyMetricsEventName.ChatSuggestionClicked,
        eventProps: { event2: true },
      });

      expect(mockTrack).toHaveBeenCalledTimes(2);
      mockTrack.mock.calls.forEach((call: any[]) => {
        expect(call[1]).toHaveProperty('user.id', 'test-user-id');
      });
    });
  });
});
