import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ConsentAPI } from './ConsentAPI';
import type { ConsentConfig, ConsentState } from '../types';

describe('ConsentAPI', () => {
  let api: ConsentAPI;
  const mockConfig: ConsentConfig = {
    apiEndpoint: 'https://api.example.com/',
    siteId: 'test-site',
    debug: false,
  };

  const mockConsent: ConsentState = {
    categories: {
      essential: true,
      functional: true,
      analytics: false,
      marketing: false,
      social: false,
    },
    vendors: {},
    timestamp: '2024-01-15T10:00:00.000Z',
    version: '1.0.0',
  };

  beforeEach(() => {
    api = new ConsentAPI(mockConfig);
    vi.clearAllMocks();
  });

  describe('constructor', () => {
    it('should strip trailing slash from apiEndpoint', () => {
      expect(api).toBeDefined();
    });
  });

  describe('saveConsent', () => {
    it('should POST consent to the API', async () => {
      const mockResponse = {
        consentId: 'consent-123',
        timestamp: '2024-01-15T10:00:00.000Z',
        expiresAt: '2025-01-15T10:00:00.000Z',
      };

      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockResponse),
      } as Response);

      const result = await api.saveConsent({
        siteId: 'test-site',
        deviceFingerprint: 'fp_123',
        consent: mockConsent,
      });

      expect(fetch).toHaveBeenCalledTimes(1);
      expect(fetch).toHaveBeenCalledWith(
        'https://api.example.com/consent',
        expect.objectContaining({
          method: 'POST',
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
            Accept: 'application/json',
          }),
          credentials: 'include',
        })
      );

      expect(result.consentId).toBe('consent-123');
    });

    it('should include metadata in the request', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve({ consentId: '123' }),
      } as Response);

      await api.saveConsent({
        siteId: 'test-site',
        deviceFingerprint: 'fp_123',
        consent: mockConsent,
      });

      const call = vi.mocked(fetch).mock.calls[0];
      const body = JSON.parse(call[1]?.body as string);

      expect(body.metadata).toBeDefined();
      expect(body.metadata.platform).toBe('web');
    });

    it('should throw on non-ok response', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 500,
      } as Response);

      await expect(
        api.saveConsent({
          siteId: 'test-site',
          deviceFingerprint: 'fp_123',
          consent: mockConsent,
        })
      ).rejects.toThrow('Failed to save consent: 500');
    });

    it('should include signature headers', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve({ consentId: '123' }),
      } as Response);

      await api.saveConsent({
        siteId: 'test-site',
        deviceFingerprint: 'fp_123',
        consent: mockConsent,
      });

      const call = vi.mocked(fetch).mock.calls[0];
      const headers = call[1]?.headers as Record<string, string>;

      expect(headers['X-Consent-Timestamp']).toBeDefined();
      expect(headers['X-Consent-Signature']).toMatch(/^sha256=/);
    });
  });

  describe('getConsent', () => {
    it('should GET consent from the API', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve({ consent: mockConsent }),
      } as Response);

      const result = await api.getConsent('test-site', 'fp_123');

      expect(fetch).toHaveBeenCalledWith(
        expect.stringContaining('/consent?siteId=test-site&deviceFingerprint=fp_123'),
        expect.objectContaining({
          headers: expect.any(Object),
          credentials: 'include',
        })
      );

      expect(result?.categories.essential).toBe(true);
    });

    it('should return null on 404', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      const result = await api.getConsent('test-site', 'fp_123');

      expect(result).toBeNull();
    });

    it('should throw on other errors', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 500,
      } as Response);

      await expect(api.getConsent('test-site', 'fp_123')).rejects.toThrow(
        'Failed to get consent: 500'
      );
    });
  });

  describe('revokeConsent', () => {
    it('should DELETE consent from the API', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 204,
      } as Response);

      await api.revokeConsent('consent-123');

      expect(fetch).toHaveBeenCalledWith(
        'https://api.example.com/consent/consent-123',
        expect.objectContaining({
          method: 'DELETE',
        })
      );
    });

    it('should throw on non-ok response', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      await expect(api.revokeConsent('consent-123')).rejects.toThrow(
        'Failed to revoke consent: 404'
      );
    });
  });

  describe('getSiteConfig', () => {
    it('should GET site configuration', async () => {
      const mockSiteConfig = {
        siteId: 'test-site',
        name: 'Test Site',
        categories: ['essential', 'analytics'],
      };

      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockSiteConfig),
      } as Response);

      const result = await api.getSiteConfig('test-site');

      expect(fetch).toHaveBeenCalledWith(
        'https://api.example.com/config/test-site',
        expect.any(Object)
      );

      expect(result.siteId).toBe('test-site');
    });

    it('should throw on error', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      await expect(api.getSiteConfig('unknown-site')).rejects.toThrow(
        'Failed to get site config: 404'
      );
    });
  });

  describe('exportConsent', () => {
    it('should GET consent export for user', async () => {
      const mockExport = {
        userId: 'user-123',
        consents: [mockConsent],
      };

      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockExport),
      } as Response);

      const result = await api.exportConsent('user-123');

      expect(fetch).toHaveBeenCalledWith(
        expect.stringContaining('/consent/export?userId=user-123'),
        expect.any(Object)
      );

      expect(result).toEqual(mockExport);
    });

    it('should throw on error', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 403,
      } as Response);

      await expect(api.exportConsent('user-123')).rejects.toThrow(
        'Failed to export consent: 403'
      );
    });
  });

  describe('network errors', () => {
    it('should propagate fetch errors', async () => {
      vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));

      await expect(
        api.saveConsent({
          siteId: 'test-site',
          deviceFingerprint: 'fp_123',
          consent: mockConsent,
        })
      ).rejects.toThrow('Network error');
    });
  });

  describe('debug mode', () => {
    it('should log when debug is enabled', async () => {
      const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

      const debugApi = new ConsentAPI({
        ...mockConfig,
        debug: true,
      });

      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: () => Promise.resolve({ consentId: '123' }),
      } as Response);

      await debugApi.saveConsent({
        siteId: 'test-site',
        deviceFingerprint: 'fp_123',
        consent: mockConsent,
      });

      expect(consoleSpy).toHaveBeenCalled();
      consoleSpy.mockRestore();
    });
  });
});
