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

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

  beforeEach(() => {
    localStorage.clear();
    vi.clearAllMocks();

    // Mock successful API response
    vi.mocked(fetch).mockResolvedValue({
      ok: true,
      status: 200,
      json: () =>
        Promise.resolve({
          consentId: 'consent-123',
          timestamp: '2024-01-15T10:00:00.000Z',
          expiresAt: '2025-01-15T10:00:00.000Z',
        }),
    } as Response);

    manager = new ConsentManager(mockConfig);
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe('constructor', () => {
    it('should create manager with merged config', () => {
      expect(manager).toBeDefined();
    });

    it('should apply default config values', () => {
      // Default consent config should be applied
      expect(manager).toBeDefined();
    });
  });

  describe('init', () => {
    it('should initialize the manager', async () => {
      await manager.init();

      // Should have generated fingerprint and be initialized
      expect(manager.needsConsent()).toBe(true); // No consent stored
    });

    it('should only initialize once', async () => {
      await manager.init();
      await manager.init(); // Second call should be skipped

      expect(manager.needsConsent()).toBe(true);
    });

    it('should emit init event', async () => {
      const callback = vi.fn();
      manager.on('init', callback);

      await manager.init();

      expect(callback).toHaveBeenCalled();
    });

    it('should load existing consent from storage', async () => {
      // Pre-set consent in storage
      const storageKey = `bp_consent_${mockConfig.siteId}`;
      const mockConsent = {
        categories: {
          essential: true,
          functional: true,
          analytics: true,
          marketing: false,
          social: false,
        },
        vendors: {},
        timestamp: new Date().toISOString(),
        version: '1.0.0',
      };

      // Create a simple hash for signature
      const data = JSON.stringify(mockConsent) + mockConfig.siteId;
      let hash = 5381;
      for (let i = 0; i < data.length; i++) {
        hash = (hash * 33) ^ data.charCodeAt(i);
      }
      const signature = (hash >>> 0).toString(16);

      localStorage.setItem(
        storageKey,
        JSON.stringify({
          version: '1',
          consent: mockConsent,
          signature,
        })
      );

      manager = new ConsentManager(mockConfig);
      await manager.init();

      expect(manager.hasConsent('analytics')).toBe(true);
    });

    it('should show banner when no consent exists', async () => {
      const callback = vi.fn();
      manager.on('banner_show', callback);

      await manager.init();

      expect(callback).toHaveBeenCalled();
      expect(manager.isBannerVisible()).toBe(true);
    });
  });

  describe('hasConsent', () => {
    it('should return true for essential without initialization', () => {
      expect(manager.hasConsent('essential')).toBe(true);
    });

    it('should return false for other categories without consent', () => {
      expect(manager.hasConsent('analytics')).toBe(false);
      expect(manager.hasConsent('marketing')).toBe(false);
    });
  });

  describe('hasVendorConsent', () => {
    it('should return false when no consent exists', () => {
      expect(manager.hasVendorConsent('google-analytics')).toBe(false);
    });
  });

  describe('getConsent', () => {
    it('should return null when no consent exists', () => {
      expect(manager.getConsent()).toBeNull();
    });

    it('should return a copy of consent state', async () => {
      await manager.init();
      await manager.acceptAll();

      const consent1 = manager.getConsent();
      const consent2 = manager.getConsent();

      expect(consent1).not.toBe(consent2); // Different objects
      expect(consent1).toEqual(consent2); // Same content
    });
  });

  describe('setConsent', () => {
    it('should set consent categories', async () => {
      await manager.init();
      await manager.setConsent({
        essential: true,
        functional: true,
        analytics: true,
        marketing: false,
        social: false,
      });

      expect(manager.hasConsent('analytics')).toBe(true);
      expect(manager.hasConsent('marketing')).toBe(false);
    });

    it('should always keep essential enabled', async () => {
      await manager.init();
      await manager.setConsent({
        essential: false, // Attempting to disable
        functional: false,
        analytics: false,
        marketing: false,
        social: false,
      });

      expect(manager.hasConsent('essential')).toBe(true);
    });

    it('should emit change event', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('change', callback);

      await manager.setConsent({
        essential: true,
        functional: true,
        analytics: true,
        marketing: false,
        social: false,
      });

      expect(callback).toHaveBeenCalled();
    });

    it('should save consent locally even on API error', async () => {
      vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));

      await manager.init();
      await manager.setConsent({
        essential: true,
        functional: false,
        analytics: true,
        marketing: false,
        social: false,
      });

      expect(manager.hasConsent('analytics')).toBe(true);
    });
  });

  describe('acceptAll', () => {
    it('should enable all categories', async () => {
      await manager.init();
      await manager.acceptAll();

      expect(manager.hasConsent('essential')).toBe(true);
      expect(manager.hasConsent('functional')).toBe(true);
      expect(manager.hasConsent('analytics')).toBe(true);
      expect(manager.hasConsent('marketing')).toBe(true);
      expect(manager.hasConsent('social')).toBe(true);
    });

    it('should emit accept_all event', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('accept_all', callback);

      await manager.acceptAll();

      expect(callback).toHaveBeenCalled();
    });

    it('should hide banner', async () => {
      await manager.init();
      expect(manager.isBannerVisible()).toBe(true);

      await manager.acceptAll();
      expect(manager.isBannerVisible()).toBe(false);
    });
  });

  describe('rejectAll', () => {
    it('should only keep essential enabled', async () => {
      await manager.init();
      await manager.rejectAll();

      expect(manager.hasConsent('essential')).toBe(true);
      expect(manager.hasConsent('functional')).toBe(false);
      expect(manager.hasConsent('analytics')).toBe(false);
      expect(manager.hasConsent('marketing')).toBe(false);
      expect(manager.hasConsent('social')).toBe(false);
    });

    it('should emit reject_all event', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('reject_all', callback);

      await manager.rejectAll();

      expect(callback).toHaveBeenCalled();
    });

    it('should hide banner', async () => {
      await manager.init();
      await manager.rejectAll();

      expect(manager.isBannerVisible()).toBe(false);
    });
  });

  describe('revokeAll', () => {
    it('should clear all consent', async () => {
      await manager.init();
      await manager.acceptAll();
      await manager.revokeAll();

      expect(manager.getConsent()).toBeNull();
    });

    it('should try to revoke on server', async () => {
      await manager.init();
      await manager.acceptAll();

      vi.mocked(fetch).mockResolvedValueOnce({
        ok: true,
        status: 204,
      } as Response);

      await manager.revokeAll();

      // DELETE request should have been made
      expect(fetch).toHaveBeenCalledWith(
        expect.stringContaining('/consent/'),
        expect.objectContaining({ method: 'DELETE' })
      );
    });
  });

  describe('exportConsent', () => {
    it('should export consent data as JSON', async () => {
      await manager.init();
      await manager.acceptAll();

      const exported = await manager.exportConsent();
      const parsed = JSON.parse(exported);

      expect(parsed.currentConsent).toBeDefined();
      expect(parsed.exportedAt).toBeDefined();
      expect(parsed.siteId).toBe('test-site');
    });
  });

  describe('needsConsent', () => {
    it('should return true when no consent exists', () => {
      expect(manager.needsConsent()).toBe(true);
    });

    it('should return false when valid consent exists', async () => {
      await manager.init();
      await manager.acceptAll();

      // After acceptAll, consent should exist
      expect(manager.getConsent()).not.toBeNull();
      // needsConsent checks for currentConsent and expiration
      // Since we just accepted all, consent should be valid
      const consent = manager.getConsent();
      expect(consent?.categories?.essential).toBe(true);
    });
  });

  describe('banner control', () => {
    it('should show banner', async () => {
      await manager.init();
      manager.hideBanner();
      manager.showBanner();

      expect(manager.isBannerVisible()).toBe(true);
    });

    it('should hide banner', async () => {
      await manager.init();
      manager.hideBanner();

      expect(manager.isBannerVisible()).toBe(false);
    });

    it('should emit banner_show event', async () => {
      const callback = vi.fn();
      manager.on('banner_show', callback);

      await manager.init(); // This shows banner

      expect(callback).toHaveBeenCalled();
    });

    it('should emit banner_hide event', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('banner_hide', callback);

      manager.hideBanner();

      expect(callback).toHaveBeenCalled();
    });

    it('should not show banner if already visible', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('banner_show', callback);

      callback.mockClear();
      manager.showBanner();
      manager.showBanner();

      expect(callback).toHaveBeenCalledTimes(0); // Already visible from init
    });
  });

  describe('showSettings', () => {
    it('should emit settings_open event', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('settings_open', callback);

      manager.showSettings();

      expect(callback).toHaveBeenCalled();
    });
  });

  describe('event handling', () => {
    it('should register event listeners', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('change', callback);

      await manager.acceptAll();

      expect(callback).toHaveBeenCalled();
    });

    it('should unregister event listeners', async () => {
      await manager.init();
      const callback = vi.fn();
      manager.on('change', callback);
      manager.off('change', callback);

      await manager.acceptAll();

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

    it('should return unsubscribe function', async () => {
      await manager.init();
      const callback = vi.fn();
      const unsubscribe = manager.on('change', callback);

      unsubscribe();
      await manager.acceptAll();

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

  describe('callbacks', () => {
    it('should call onConsentChange callback', async () => {
      const onConsentChange = vi.fn();
      manager = new ConsentManager({
        ...mockConfig,
        onConsentChange,
      });

      await manager.init();
      await manager.acceptAll();

      expect(onConsentChange).toHaveBeenCalled();
    });

    it('should call onBannerShow callback', async () => {
      const onBannerShow = vi.fn();
      manager = new ConsentManager({
        ...mockConfig,
        onBannerShow,
      });

      await manager.init();

      expect(onBannerShow).toHaveBeenCalled();
    });

    it('should call onBannerHide callback', async () => {
      const onBannerHide = vi.fn();
      manager = new ConsentManager({
        ...mockConfig,
        onBannerHide,
      });

      await manager.init();
      manager.hideBanner();

      expect(onBannerHide).toHaveBeenCalled();
    });
  });

  describe('static methods', () => {
    it('should return SDK version', () => {
      const version = ConsentManager.getVersion();

      expect(version).toBeDefined();
      expect(typeof version).toBe('string');
    });
  });

  describe('Google Consent Mode', () => {
    it('should update Google Consent Mode when gtag is available', async () => {
      const gtag = vi.fn();
      (window as unknown as { gtag: typeof gtag }).gtag = gtag;

      await manager.init();
      await manager.acceptAll();

      expect(gtag).toHaveBeenCalledWith(
        'consent',
        'update',
        expect.objectContaining({
          analytics_storage: 'granted',
          ad_storage: 'granted',
        })
      );

      delete (window as unknown as { gtag?: typeof gtag }).gtag;
    });
  });

  describe('consent expiration', () => {
    it('should clear expired consent on init', async () => {
      const storageKey = `bp_consent_${mockConfig.siteId}`;
      const expiredConsent = {
        categories: {
          essential: true,
          functional: true,
          analytics: true,
          marketing: false,
          social: false,
        },
        vendors: {},
        timestamp: '2020-01-01T00:00:00.000Z', // Very old
        version: '1.0.0',
        expiresAt: '2020-06-01T00:00:00.000Z', // Expired
      };

      const data = JSON.stringify(expiredConsent) + mockConfig.siteId;
      let hash = 5381;
      for (let i = 0; i < data.length; i++) {
        hash = (hash * 33) ^ data.charCodeAt(i);
      }
      const signature = (hash >>> 0).toString(16);

      localStorage.setItem(
        storageKey,
        JSON.stringify({
          version: '1',
          consent: expiredConsent,
          signature,
        })
      );

      manager = new ConsentManager(mockConfig);
      await manager.init();

      expect(manager.needsConsent()).toBe(true);
    });
  });

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

      const debugManager = new ConsentManager({
        ...mockConfig,
        debug: true,
      });

      await debugManager.init();

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

  describe('consent input normalization', () => {
    it('should accept categories object directly', async () => {
      await manager.init();
      await manager.setConsent({
        essential: true,
        functional: true,
        analytics: false,
        marketing: false,
        social: false,
      });

      expect(manager.hasConsent('functional')).toBe(true);
    });

    it('should accept nested categories object', async () => {
      await manager.init();
      await manager.setConsent({
        categories: {
          essential: true,
          functional: false,
          analytics: true,
          marketing: false,
          social: false,
        },
      });

      expect(manager.hasConsent('analytics')).toBe(true);
      expect(manager.hasConsent('functional')).toBe(false);
    });

    it('should accept vendors in consent input', async () => {
      await manager.init();
      await manager.setConsent({
        categories: {
          essential: true,
          functional: true,
          analytics: true,
          marketing: false,
          social: false,
        },
        vendors: {
          'google-analytics': true,
        },
      });

      const consent = manager.getConsent();
      expect(consent?.vendors['google-analytics']).toBe(true);
    });
  });
});
