import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
  DiagnosticsClient,
  BreadcrumbTrail,
  NetworkInterceptor,
} from '../index.js';

describe('DiagnosticsClient', () => {
  const mockConfig = {
    productId: 'test-app',
    anonymousInstallId: 'install_123',
    platform: 'web',
    channel: 'web_app',
    osFamily: 'macos',
    appVersion: '1.0.0',
    buildNumber: '100',
    releaseChannel: 'beta',
    serverUrl: 'https://api.test.com',
    pollIntervalMs: 100, // Fast polling for tests
    logger: {
      debug: vi.fn(),
      info: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    },
  };

  let fetchMock: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    DiagnosticsClient.reset();
    vi.clearAllMocks();
    
    // Mock fetch to return empty session (no active debug session)
    fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      headers: new Map(),
      json: async () => null, // No active session
    });
    globalThis.fetch = fetchMock;
  });

  afterEach(() => {
    DiagnosticsClient.reset();
    vi.restoreAllMocks();
  });

  describe('singleton', () => {
    it('should create instance with getInstance', () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      expect(client).toBeDefined();
      expect(DiagnosticsClient.isInitialized()).toBe(true);
    });

    it('should return same instance on subsequent calls', () => {
      const client1 = DiagnosticsClient.getInstance(mockConfig);
      const client2 = DiagnosticsClient.getInstance();
      expect(client1).toBe(client2);
    });

    it('should throw if getInstance called without config first', () => {
      expect(() => DiagnosticsClient.getInstance()).toThrow(
        'must be initialized with config first'
      );
    });

    it('should reset instance', () => {
      DiagnosticsClient.getInstance(mockConfig);
      expect(DiagnosticsClient.isInitialized()).toBe(true);
      DiagnosticsClient.reset();
      expect(DiagnosticsClient.isInitialized()).toBe(false);
    });
  });

  describe('lifecycle', () => {
    it('should start and stop', async () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      await client.start();
      // After start, state should be polling (no active session from mock)
      expect(client.getState().type).toBe('polling');
      client.stop();
      expect(client.getState().type).toBe('idle');
    });

    it('should warn if started twice', async () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      await client.start();
      // First clear any calls from the first start
      mockConfig.logger.warn.mockClear();
      // Second start should warn
      await client.start();
      expect(mockConfig.logger.warn).toHaveBeenCalledWith(
        '[diagnostics] Already started'
      );
    });
  });

  describe('session state', () => {
    it('should report no active session initially', () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      expect(client.isSessionActive()).toBe(false);
      expect(client.getCurrentSession()).toBeNull();
    });
  });

  describe('logging', () => {
    it('should record log entries', () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      client.log('info', 'Test message', { foo: 'bar' });
      // Logs are buffered, no immediate assertion
      expect(client.getBreadcrumbs().length).toBeGreaterThan(0);
    });

    it('should add breadcrumb on log', () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      client.log('warn', 'Warning message');
      const crumbs = client.getBreadcrumbs();
      expect(crumbs.some(c => c.message.includes('Warning'))).toBe(true);
    });
  });

  describe('tracing', () => {
    it('should trace successful operation', async () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      const result = await client.trace('test-op', () => 'success');
      expect(result).toBe('success');
    });

    it('should trace async operation', async () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      const result = await client.trace('async-op', async () => {
        await new Promise(r => setTimeout(r, 10));
        return 42;
      });
      expect(result).toBe(42);
    });

    it('should propagate errors', async () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      await expect(
        client.trace('failing-op', () => {
          throw new Error('test error');
        })
      ).rejects.toThrow('test error');
    });
  });

  describe('breadcrumbs', () => {
    it('should add breadcrumbs', () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      client.breadcrumb('navigation', 'Page loaded', { path: '/' });
      const crumbs = client.getBreadcrumbs();
      expect(crumbs.length).toBe(1);
      expect(crumbs[0].category).toBe('navigation');
      expect(crumbs[0].message).toBe('Page loaded');
    });

    it('should include data in breadcrumbs', () => {
      const client = DiagnosticsClient.getInstance(mockConfig);
      client.breadcrumb('user', 'Clicked', { button: 'submit' });
      const crumbs = client.getBreadcrumbs();
      expect(crumbs[0].data).toEqual({ button: 'submit' });
    });
  });
});

describe('BreadcrumbTrail', () => {
  it('should add breadcrumbs', () => {
    const trail = new BreadcrumbTrail();
    trail.add('test', 'message');
    expect(trail.size()).toBe(1);
  });

  it('should evict oldest when over limit', () => {
    const trail = new BreadcrumbTrail({ maxSize: 3 });
    trail.add('a', '1');
    trail.add('b', '2');
    trail.add('c', '3');
    trail.add('d', '4'); // Should evict 'a'
    expect(trail.size()).toBe(3);
    const all = trail.getAll();
    expect(all[0].category).toBe('b');
  });

  it('should get last N breadcrumbs', () => {
    const trail = new BreadcrumbTrail();
    trail.add('a', '1');
    trail.add('b', '2');
    trail.add('c', '3');
    const last2 = trail.getLast(2);
    expect(last2.length).toBe(2);
    expect(last2[0].category).toBe('b');
  });

  it('should get most recent', () => {
    const trail = new BreadcrumbTrail();
    trail.add('a', '1');
    trail.add('b', '2');
    const recent = trail.getMostRecent();
    expect(recent?.category).toBe('b');
  });

  it('should clear all', () => {
    const trail = new BreadcrumbTrail();
    trail.add('a', '1');
    trail.clear();
    expect(trail.size()).toBe(0);
  });
});

describe('NetworkInterceptor', () => {
  it('should start and stop', () => {
    const onRequest = vi.fn();
    const interceptor = new NetworkInterceptor(onRequest);
    interceptor.start();
    expect(interceptor.isRunning()).toBe(true);
    interceptor.stop();
    expect(interceptor.isRunning()).toBe(false);
  });

  it('should not capture when stopped', () => {
    const onRequest = vi.fn();
    const interceptor = new NetworkInterceptor(onRequest);
    expect(interceptor.isRunning()).toBe(false);
  });
});
