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

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

// Set env var in vi.hoisted so it runs before static import of telemetry module
vi.hoisted(() => {
  process.env.NEXT_PUBLIC_PRODUCT_ID = 'test-product';
});

// 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: vi.fn() });
vi.stubGlobal('window', {
  addEventListener: mockAddEventListener,
});

import { trackEvent, trackPageView, flush, initTelemetry } from '@/lib/telemetry';

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

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

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

    expect(mockSendBeacon).toHaveBeenCalledTimes(1);
    const [url, body] = mockSendBeacon.mock.calls[0];
    expect(url).toBe('/api/telemetry/admin-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('admin');
    expect(event.eventName).toBe('filter_applied');
    expect(event.id).toBeDefined();
    expect(event.sessionId).toBeDefined();
    expect(event.anonymousInstallId).toBeDefined();
    expect(event.occurredAt).toBeDefined();
  });

  it('includes optional fields when provided', () => {
    trackEvent('warn', 'admin', 'policy_changed', {
      message: 'Policy updated',
      tags: { policyId: 'pol_1' },
      metrics: { duration_ms: 120 },
    });
    flush();

    const payload = JSON.parse(mockSendBeacon.mock.calls[0][1]);
    const event = payload.events[0];
    expect(event.message).toBe('Policy updated');
    expect(event.tags.policyId).toBe('pol_1');
    expect(event.metrics.duration_ms).toBe(120);
  });

  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('/ops/client-logs');
    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('/ops/client-logs');
  });
});

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

describe('flush', () => {
  it('uses sendBeacon to admin-ingest route', () => {
    trackEvent('info', 'test', 'test_event');
    flush();

    expect(mockSendBeacon).toHaveBeenCalledWith('/api/telemetry/admin-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/admin-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();
    // initTelemetry registers on document (visibilitychange) via the shared client
    expect(document.addEventListener).toHaveBeenCalled();
  });

  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).toBeDefined();
    expect(sessionEvent.module).toBe('app_lifecycle');
  });
});
