/**
 * Tests for client-side telemetry module (src/lib/telemetry.ts) — tracker dashboard.
 * Verifies event format, queue behavior, flush logic, and install ID.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

// Mock process.env BEFORE importing telemetry module (hoisted)
const mockProductId = vi.hoisted(() => 'test-product');
vi.stubGlobal('process', {
  env: { NEXT_PUBLIC_PRODUCT_ID: mockProductId },
});

// Mock browser globals before importing the module
const mockSendBeacon = vi.fn().mockReturnValue(true);
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
const mockAddEventListener = vi.fn();

vi.stubGlobal('navigator', { sendBeacon: mockSendBeacon, userAgent: 'TestAgent/1.0' });
vi.stubGlobal('fetch', mockFetch);
vi.stubGlobal('crypto', {
  randomUUID: () => '550e8400-e29b-41d4-a716-446655440000',
});

const mockLocalStorage = new Map<string, string>();
vi.stubGlobal('localStorage', {
  getItem: (key: string) => mockLocalStorage.get(key) ?? null,
  setItem: (key: string, value: string) => mockLocalStorage.set(key, value),
  removeItem: (key: string) => mockLocalStorage.delete(key),
});

vi.stubGlobal('document', {
  visibilityState: 'visible',
  addEventListener: mockAddEventListener,
});
vi.stubGlobal('window', {
  addEventListener: mockAddEventListener,
});

// Dynamic import to ensure mocks are set up first
const { trackEvent, trackPageView, flush, initTelemetry } = await import('@/lib/telemetry');

beforeEach(() => {
  mockSendBeacon.mockClear();
  mockFetch.mockClear();
  mockAddEventListener.mockClear();
  mockLocalStorage.clear();
});

// ─── trackEvent ─────────────────────────────────────────────────────

describe('trackEvent', () => {
  it('queues an event with correct fields', () => {
    trackEvent('info', 'tracker', 'item_created');
    flush();

    expect(mockSendBeacon).toHaveBeenCalledTimes(1);
    const [url, body] = mockSendBeacon.mock.calls[0];
    expect(url).toBe('/api/telemetry/ingest');

    const payload = JSON.parse(body);
    expect(payload.productId).toBe('test-product');
    expect(payload.events).toHaveLength(1);

    const event = payload.events[0];
    expect(event.platform).toBe('web');
    expect(event.channel).toBe('web_app');
    expect(event.osFamily).toBe('other');
    expect(event.eventType).toBe('info');
    expect(event.module).toBe('tracker');
    expect(event.eventName).toBe('item_created');
    expect(event.id).toBe('550e8400-e29b-41d4-a716-446655440000');
    expect(typeof event.sessionId).toBe('string');
    expect(event.anonymousInstallId).toBe('550e8400-e29b-41d4-a716-446655440000');
    expect(event.occurredAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
  });

  it('includes optional fields when provided', () => {
    trackEvent('error', 'tracker', 'submit_failed', {
      message: 'Network error',
      tags: { board: 'bugs' },
      metrics: { retryCount: 3 },
    });
    flush();

    const payload = JSON.parse(mockSendBeacon.mock.calls[0][1]);
    const event = payload.events[0];
    expect(event.message).toBe('Network error');
    expect(event.tags.board).toBe('bugs');
    expect(event.metrics.retryCount).toBe(3);
  });

  it('osFamily is "other" not "web"', () => {
    trackEvent('info', 'test', 'test_event');
    flush();

    const payload = JSON.parse(mockSendBeacon.mock.calls[0][1]);
    expect(payload.events[0].osFamily).toBe('other');
  });
});

// ─── trackPageView ──────────────────────────────────────────────────

describe('trackPageView', () => {
  it('creates page_view event with path tag', () => {
    trackPageView('/board/bugs');
    flush();

    const payload = JSON.parse(mockSendBeacon.mock.calls[0][1]);
    const event = payload.events[0];
    expect(event.module).toBe('navigation');
    expect(event.eventName).toBe('page_view');
    expect(event.tags.path).toBe('/board/bugs');
  });
});

// ─── flush ──────────────────────────────────────────────────────────

describe('flush', () => {
  it('uses sendBeacon', () => {
    trackEvent('info', 'test', 'test_event');
    flush();

    expect(mockSendBeacon).toHaveBeenCalledWith('/api/telemetry/ingest', expect.any(String));
  });

  it('falls back to fetch when sendBeacon fails', () => {
    mockSendBeacon.mockReturnValueOnce(false);
    trackEvent('info', 'test', 'test_event');
    flush();

    expect(mockFetch).toHaveBeenCalledWith(
      '/api/telemetry/ingest',
      expect.objectContaining({ method: 'POST', keepalive: true })
    );
  });

  it('does nothing when queue is empty', () => {
    flush();
    expect(mockSendBeacon).not.toHaveBeenCalled();
  });

  it('clears queue after flush', () => {
    trackEvent('info', 'test', 'event1');
    trackEvent('info', 'test', 'event2');
    flush();

    mockSendBeacon.mockClear();
    flush();
    expect(mockSendBeacon).not.toHaveBeenCalled();
  });
});

// ─── initTelemetry ──────────────────────────────────────────────────

describe('initTelemetry', () => {
  afterEach(() => {
    flush();
  });

  it('registers visibilitychange listener', () => {
    initTelemetry();
    expect(mockAddEventListener).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
  });

  it('tracks session_started event', () => {
    initTelemetry();
    flush();

    const payload = JSON.parse(mockSendBeacon.mock.calls[0][1]);
    const sessionEvent = payload.events.find(
      (e: Record<string, string>) => e.eventName === 'session_started'
    );
    expect(sessionEvent).toEqual(
      expect.objectContaining({
        module: 'app_lifecycle',
        eventType: 'info',
        eventName: 'session_started',
      })
    );
  });
});
