/**
 * PresetImportExportManager tests
 * Story 3.3: Preset Import & Export
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { PresetImportExportManager } from '../../../src/core/PresetImportExportManager.js';
import { createScenePreset, SCENE_PRESET_VERSION, MAX_PRESETS_PER_WORLD } from '../../../src/contracts/scene-preset.js';

// Mock FoundryAdapter
const createMockAdapter = () => ({
  scenes: {
    current: () => ({ name: 'Test Scene', id: 'test-scene' }),
  },
});

// Mock ScenePresetManager
const createMockScenePresetManager = (presets = []) => {
  const cache = new Map();
  presets.forEach(p => cache.set(p.name, p));
  
  return {
    list: () => Array.from(cache.values()),
    save: vi.fn(async (name) => {
      const preset = createScenePreset(name, {});
      cache.set(name, preset);
      return preset;
    }),
    delete: vi.fn(async (name) => {
      cache.delete(name);
    }),
    get: vi.fn((name) => cache.get(name) ?? null),
    _presetsCache: cache,
    _saveScenePresets: vi.fn(async () => {}),
  };
};

describe('PresetImportExportManager', () => {
  let adapter;
  let scenePresetManager;
  let manager;

  beforeEach(() => {
    adapter = createMockAdapter();
    scenePresetManager = createMockScenePresetManager();
    manager = new PresetImportExportManager(adapter, scenePresetManager);
  });

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

  // ========================================================================
  // Constructor Tests
  // ========================================================================

  describe('Constructor', () => {
    it('should accept valid adapter and scenePresetManager', () => {
      expect(() => {
        new PresetImportExportManager(adapter, scenePresetManager);
      }).not.toThrow();
    });

    it('should throw TypeError if adapter is missing', () => {
      expect(() => {
        new PresetImportExportManager(null, scenePresetManager);
      }).toThrow(TypeError);
    });

    it('should throw TypeError if adapter is not an object', () => {
      expect(() => {
        new PresetImportExportManager('not-an-object', scenePresetManager);
      }).toThrow(TypeError);
    });

    it('should throw TypeError if scenePresetManager is missing', () => {
      expect(() => {
        new PresetImportExportManager(adapter, null);
      }).toThrow(TypeError);
    });

    it('should throw TypeError if scenePresetManager is not an object', () => {
      expect(() => {
        new PresetImportExportManager(adapter, 'not-an-object');
      }).toThrow(TypeError);
    });
  });

  // ========================================================================
  // exportAllPresets Tests
  // ========================================================================

  describe('exportAllPresets', () => {
    it('should export empty presets when no presets exist', async () => {
      const result = await manager.exportAllPresets();
      const parsed = JSON.parse(result);
      
      expect(parsed._version).toBe(1);
      expect(parsed.worldName).toBe('Test Scene');
      expect(parsed.exportedAt).toBeTypeOf('number');
      expect(parsed.presets).toEqual({});
    });

    it('should export all presets with correct structure', async () => {
      const preset1 = createScenePreset('Preset 1', { user1: 'active' });
      const preset2 = createScenePreset('Preset 2', { user2: 'hidden' });
      scenePresetManager = createMockScenePresetManager([preset1, preset2]);
      manager = new PresetImportExportManager(adapter, scenePresetManager);

      const result = await manager.exportAllPresets();
      const parsed = JSON.parse(result);
      
      expect(parsed._version).toBe(1);
      expect(parsed.worldName).toBe('Test Scene');
      expect(parsed.exportedAt).toBeTypeOf('number');
      expect(Object.keys(parsed.presets)).toHaveLength(2);
      expect(parsed.presets['Preset 1']).toBeDefined();
      expect(parsed.presets['Preset 2']).toBeDefined();
    });

    it('should include preset data in export', async () => {
      const matrix = { user1: 'active', user2: 'hidden' };
      const preset = createScenePreset('Test Preset', matrix);
      scenePresetManager = createMockScenePresetManager([preset]);
      manager = new PresetImportExportManager(adapter, scenePresetManager);

      const result = await manager.exportAllPresets();
      const parsed = JSON.parse(result);
      
      expect(parsed.presets['Test Preset'].name).toBe('Test Preset');
      expect(parsed.presets['Test Preset'].matrix).toEqual(matrix);
      expect(parsed.presets['Test Preset']._version).toBe(SCENE_PRESET_VERSION);
    });
  });

  // ========================================================================
  // generateExportFilename Tests
  // ========================================================================

  describe('generateExportFilename', () => {
    it('should generate filename with world name and timestamp by default', () => {
      const filename = manager.generateExportFilename('My Campaign');
      
      expect(filename).toMatch(/^scrying-pool-presets-my_campaign_\d+\.json$/);
    });

    it('should generate filename without timestamp when disabled', () => {
      const filename = manager.generateExportFilename('My Campaign', false);
      
      expect(filename).toBe('scrying-pool-presets-my_campaign.json');
    });

    it('should use default world name when not provided', () => {
      const filename = manager.generateExportFilename(undefined, false);
      
      // Defaults to current scene name from adapter which is "Test Scene"
      expect(filename).toBe('scrying-pool-presets-test_scene.json');
    });

    it('should sanitize world name for filesystem safety', () => {
      const filename = manager.generateExportFilename('My/Campaign:Name*Test?', false);
      
      // All special characters (/, :, *, ?) are replaced with single underscores
      expect(filename).toBe('scrying-pool-presets-my_campaign_name_test_.json');
    });

    it('should handle empty world name', () => {
      const filename = manager.generateExportFilename('', false);
      
      // Empty string becomes empty after replace, then falls back to 'world'
      expect(filename).toBe('scrying-pool-presets-world.json');
    });
  });

  // ========================================================================
  // validateImportData Tests
  // ========================================================================

  describe('validateImportData', () => {
    it('should accept valid import data with presets', () => {
      const data = {
        _version: 1,
        worldName: 'Test World',
        exportedAt: 1234567890,
        presets: {
          Preset1: createScenePreset('Preset1', {}),
        },
      };
      
      const result = manager.validateImportData(data);
      expect(result).toEqual(data);
    });

    it('should accept valid import data with only required fields', () => {
      const data = {
        _version: 1,
        presets: {},
      };
      
      expect(() => manager.validateImportData(data)).not.toThrow();
    });

    it('should reject null data', () => {
      expect(() => manager.validateImportData(null)).toThrow(TypeError);
    });

    it('should reject non-object data', () => {
      expect(() => manager.validateImportData('string')).toThrow(TypeError);
      expect(() => manager.validateImportData(123)).toThrow(TypeError);
      expect(() => manager.validateImportData([])).toThrow(TypeError);
    });

    it('should reject missing _version', () => {
      const data = { presets: {} };
      
      expect(() => manager.validateImportData(data)).toThrow(TypeError);
    });

    it('should reject invalid _version type', () => {
      const data = { _version: '1', presets: {} };
      
      expect(() => manager.validateImportData(data)).toThrow(TypeError);
    });

    it('should reject unsupported schema version', () => {
      const data = { _version: 2, presets: {} };
      
      expect(() => manager.validateImportData(data)).toThrow(TypeError);
    });

    it('should reject missing presets', () => {
      const data = { _version: 1 };
      
      expect(() => manager.validateImportData(data)).toThrow(TypeError);
    });

    it('should reject presets as array', () => {
      const data = { _version: 1, presets: [] };
      
      expect(() => manager.validateImportData(data)).toThrow(TypeError);
    });

    it('should accept valid worldName if present', () => {
      const data = { _version: 1, worldName: 'Valid Name', presets: {} };
      
      expect(() => manager.validateImportData(data)).not.toThrow();
    });

    it('should reject invalid worldName type', () => {
      const data = { _version: 1, worldName: 123, presets: {} };
      
      expect(() => manager.validateImportData(data)).toThrow(TypeError);
    });

    it('should accept valid exportedAt if present', () => {
      const data = { _version: 1, exportedAt: 1234567890, presets: {} };
      
      expect(() => manager.validateImportData(data)).not.toThrow();
    });

    it('should reject invalid exportedAt type', () => {
      const data = { _version: 1, exportedAt: '1234567890', presets: {} };
      
      expect(() => manager.validateImportData(data)).toThrow(TypeError);
    });
  });

  // ========================================================================
  // importPresets Tests (Merge Mode)
  // ========================================================================

  describe('importPresets (merge mode)', () => {
    it('should reject invalid mode', async () => {
      await expect(manager.importPresets('{"_version":1,"presets":{}}', 'invalid'))
        .rejects.toThrow(TypeError);
    });

    it('should reject invalid JSON', async () => {
      await expect(manager.importPresets('not valid json', 'merge'))
        .rejects.toThrow(TypeError);
    });

    it('should reject invalid JSON structure', async () => {
      await expect(manager.importPresets('{"invalid":true}', 'merge'))
        .rejects.toThrow(TypeError);
    });

    it('should merge new presets successfully', async () => {
      const preset1 = createScenePreset('Existing Preset', { user1: 'active' });
      scenePresetManager = createMockScenePresetManager([preset1]);
      manager = new PresetImportExportManager(adapter, scenePresetManager);

      const importData = {
        _version: 1,
        worldName: 'Import World',
        exportedAt: 1234567890,
        presets: {
          'New Preset': createScenePreset('New Preset', { user2: 'hidden' }),
        },
      };

      const result = await manager.importPresets(JSON.stringify(importData), 'merge');
      
      expect(result.success).toBe(true);
      expect(result.added).toBe(1);
      expect(result.skipped).toBe(0);
      expect(result.replaced).toBe(0);
      expect(result.errors).toHaveLength(0);
    });

    it('should skip duplicate presets in merge mode', async () => {
      const preset1 = createScenePreset('Existing Preset', { user1: 'active' });
      scenePresetManager = createMockScenePresetManager([preset1]);
      manager = new PresetImportExportManager(adapter, scenePresetManager);

      const importData = {
        _version: 1,
        presets: {
          'Existing Preset': createScenePreset('Existing Preset', { user2: 'hidden' }),
          'New Preset': createScenePreset('New Preset', { user3: 'active' }),
        },
      };

      const result = await manager.importPresets(JSON.stringify(importData), 'merge');
      
      expect(result.success).toBe(true);
      expect(result.added).toBe(1);
      expect(result.skipped).toBe(1);
      expect(result.message).toContain('skipped as duplicates');
    });

    it('should report invalid presets in import but continue with valid ones', async () => {
      const importData = {
        _version: 1,
        presets: {
          'Valid Preset': createScenePreset('Valid Preset', {}),
          'Invalid Preset': { _version: 1, name: '' }, // Invalid: empty name
        },
      };

      const result = await manager.importPresets(JSON.stringify(importData), 'merge');
      
      // Invalid presets produce extraction errors; valid presets still import
      expect(result.success).toBe(false); // Extraction errors present
      expect(result.added).toBe(1); // One valid preset added
      expect(result.errors.length).toBeGreaterThan(0);
      expect(result.errors.some(e => e.includes('Invalid Preset'))).toBe(true);
    });

    it('should reject import exceeding max presets limit', async () => {
      // Create a manager with many presets
      const presets = [];
      for (let i = 0; i < MAX_PRESETS_PER_WORLD; i++) {
        presets.push(createScenePreset(`Preset ${i}`, {}));
      }
      scenePresetManager = createMockScenePresetManager(presets);
      manager = new PresetImportExportManager(adapter, scenePresetManager);

      const importData = {
        _version: 1,
        presets: {
          'New Preset': createScenePreset('New Preset', {}),
        },
      };

      const result = await manager.importPresets(JSON.stringify(importData), 'merge');
      
      expect(result.success).toBe(false);
      expect(result.message).toContain('would exceed');
      expect(result.errors.length).toBeGreaterThan(0);
    });
  });

  // ========================================================================
  // importPresets Tests (Replace Mode)
  // ========================================================================

  describe('importPresets (replace mode)', () => {
    it('should replace all existing presets', async () => {
      const preset1 = createScenePreset('Existing Preset 1', { user1: 'active' });
      const preset2 = createScenePreset('Existing Preset 2', { user2: 'active' });
      scenePresetManager = createMockScenePresetManager([preset1, preset2]);
      manager = new PresetImportExportManager(adapter, scenePresetManager);

      const importData = {
        _version: 1,
        presets: {
          'New Preset 1': createScenePreset('New Preset 1', { user3: 'active' }),
          'New Preset 2': createScenePreset('New Preset 2', { user4: 'active' }),
        },
      };

      const result = await manager.importPresets(JSON.stringify(importData), 'replace');
      
      expect(result.success).toBe(true);
      expect(result.added).toBe(2);
      expect(result.replaced).toBe(2);
      expect(result.skipped).toBe(0);
      expect(result.message).toContain('Replaced all');
    });

    it('should handle empty import in replace mode', async () => {
      const preset1 = createScenePreset('Existing Preset', { user1: 'active' });
      scenePresetManager = createMockScenePresetManager([preset1]);
      manager = new PresetImportExportManager(adapter, scenePresetManager);

      const importData = {
        _version: 1,
        presets: {},
      };

      const result = await manager.importPresets(JSON.stringify(importData), 'replace');
      
      expect(result.success).toBe(true);
      expect(result.added).toBe(0);
      expect(result.replaced).toBe(1);
    });
  });

  // ========================================================================
  // downloadExportFile Tests
  // ========================================================================

  describe('downloadExportFile', () => {
    let originalCreateObjectURL;
    let originalRevokeObjectURL;
    let mockUrl;

    beforeEach(() => {
      originalCreateObjectURL = URL.createObjectURL;
      originalRevokeObjectURL = URL.revokeObjectURL;
      mockUrl = 'blob:mock-url';
      
      URL.createObjectURL = vi.fn(() => mockUrl);
      URL.revokeObjectURL = vi.fn();
      
      // Mock DOM
      document.body.appendChild = vi.fn();
      document.body.removeChild = vi.fn();
    });

    afterEach(() => {
      URL.createObjectURL = originalCreateObjectURL;
      URL.revokeObjectURL = originalRevokeObjectURL;
      vi.restoreAllMocks();
    });

    it('should download file with correct name', () => {
      const jsonString = '{"test": true}';
      const filename = 'test-export.json';
      
      // Mock click
      const mockA = { href: '', download: '', click: vi.fn() };
      document.createElement = vi.fn(() => mockA);

      manager.downloadExportFile(jsonString, filename);
      
      expect(mockA.download).toBe(filename);
      expect(mockA.click).toHaveBeenCalled();
      expect(URL.createObjectURL).toHaveBeenCalled();
    });

    it('should throw TypeError for invalid jsonString', () => {
      expect(() => {
        manager.downloadExportFile(null, 'test.json');
      }).toThrow(TypeError);
    });

    it('should throw TypeError for invalid filename', () => {
      expect(() => {
        manager.downloadExportFile('{}', '');
      }).toThrow(TypeError);
    });
  });
});
