const fc = require('fast-check');
const ConfigManager = require('./config-manager');
const fs = require('fs').promises;
const path = require('path');
const os = require('os');

/**
 * Property-based tests for ConfigManager
 * These tests validate universal properties across all valid inputs
 */
describe('ConfigManager Property Tests', () => {
  let manager;
  let testDir;

  beforeEach(async () => {
    manager = new ConfigManager();
    testDir = path.join(os.tmpdir(), `dashcaddy-prop-test-${Date.now()}`);
  });

  afterEach(async () => {
    try {
      await fs.rm(testDir, { recursive: true, force: true });
    } catch (error) {
      // Ignore cleanup errors
    }
  });

  /**
   * Feature: dashcaddy-installer, Property 5: Configuration Persistence
   * For any installation configuration, saving and then loading the configuration 
   * should produce an equivalent configuration object.
   * Validates: Requirements 2.6, 10.1, 10.2
   */
  describe('Property 5: Configuration Persistence', () => {
    // Generator for valid configuration objects
    const configArbitrary = () => fc.record({
      installPath: fc.constant(testDir),
      tier: fc.constantFrom('basic', 'intermediate', 'advanced'),
      dashboardName: fc.string({ minLength: 1, maxLength: 50 }),
      customTLD: fc.option(fc.string({ minLength: 2, maxLength: 10 }).map(s => '.' + s)),
      caddyAdminUrl: fc.constant('http://localhost:2021'),
      apiPort: fc.integer({ min: 1024, max: 65535 }),
      dnsEnabled: fc.boolean(),
      tailscaleEnabled: fc.boolean()
    });

    test('configuration round-trips correctly', async () => {
      await fc.assert(
        fc.asyncProperty(
          configArbitrary(),
          async (config) => {
            // Save configuration
            const saveResult = await manager.saveConfig(config, testDir);
            if (!saveResult.success) return true; // Skip if save failed
            
            // Load configuration
            const loadResult = await manager.loadConfig(testDir);
            if (!loadResult.exists) return false;
            
            // Verify all original fields are preserved
            const loaded = loadResult.config;
            return (
              loaded.installPath === config.installPath &&
              loaded.tier === config.tier &&
              loaded.dashboardName === config.dashboardName &&
              loaded.apiPort === config.apiPort &&
              loaded.dnsEnabled === config.dnsEnabled &&
              loaded.tailscaleEnabled === config.tailscaleEnabled
            );
          }
        ),
        { numRuns: 100 }
      );
    });

    test('saved config always includes metadata', async () => {
      await fc.assert(
        fc.asyncProperty(
          configArbitrary(),
          async (config) => {
            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);
            
            if (!result.exists) return true; // Skip if load failed
            
            return (
              typeof result.config.version === 'string' &&
              typeof result.config.lastModified === 'string'
            );
          }
        ),
        { numRuns: 100 }
      );
    });

    test('loading non-existent config always returns exists=false', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 }),
          async (randomPath) => {
            const nonExistentPath = path.join(testDir, randomPath);
            const result = await manager.loadConfig(nonExistentPath);
            
            return result.exists === false && result.config === null;
          }
        ),
        { numRuns: 50 }
      );
    });
  });

  /**
   * Feature: dashcaddy-installer, Property 3: Installation Path Validation
   * For any selected folder path, the installer should correctly determine 
   * if the path is writable before proceeding with installation.
   * Validates: Requirements 2.3, 2.4
   */
  describe('Property 3: Installation Path Validation', () => {
    test('validatePath returns consistent structure', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 50 }),
          async (pathSegment) => {
            const testPath = path.join(testDir, pathSegment);
            const result = await manager.validatePath(testPath);
            
            return (
              typeof result === 'object' &&
              typeof result.valid === 'boolean' &&
              typeof result.message === 'string'
            );
          }
        ),
        { numRuns: 50 }
      );
    });

    test('valid paths are consistently validated', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 }),
          async (pathSegment) => {
            const testPath = path.join(testDir, pathSegment);
            
            // Validate twice
            const result1 = await manager.validatePath(testPath);
            const result2 = await manager.validatePath(testPath);
            
            // Results should be consistent
            return result1.valid === result2.valid;
          }
        ),
        { numRuns: 30 }
      );
    });
  });

  /**
   * Feature: dashcaddy-installer, Property 4: Directory Structure Creation
   * For any valid installation path, the installer should create all required 
   * subdirectories (config, data, logs, caddyfile) and verify their existence.
   * Validates: Requirements 2.5
   */
  describe('Property 4: Directory Structure Creation', () => {
    test('createDirectories creates all required directories', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 }),
          async (pathSegment) => {
            const installPath = path.join(testDir, pathSegment);
            const result = await manager.createDirectories(installPath);
            
            if (!result.success) return true; // Skip if creation failed
            
            // Verify all required directories exist
            const requiredDirs = ['config', 'data', 'logs', 'caddyfile'];
            const checks = await Promise.all(
              requiredDirs.map(async (dir) => {
                const dirPath = path.join(installPath, dir);
                try {
                  await fs.access(dirPath);
                  return true;
                } catch {
                  return false;
                }
              })
            );
            
            return checks.every(check => check === true);
          }
        ),
        { numRuns: 50 }
      );
    });

    test('createDirectories is idempotent', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('\0') && !s.includes('|')),
          async (pathSegment) => {
            const installPath = path.join(testDir, pathSegment);
            
            // Create directories twice
            const result1 = await manager.createDirectories(installPath);
            const result2 = await manager.createDirectories(installPath);
            
            // Both should succeed (or both fail for invalid paths)
            return result1.success === result2.success;
          }
        ),
        { numRuns: 30 }
      );
    });
  });

  /**
   * Feature: dashcaddy-installer, Property 8: DNS Credential Security
   * For any DNS credentials, saving and then loading the credentials should 
   * preserve all fields, and the password should be encrypted in storage.
   * Validates: Requirements 4.8, 10.3
   */
  describe('Property 8: DNS Credential Security', () => {
    const credentialsArbitrary = () => fc.record({
      server: fc.ipV4(),
      username: fc.string({ minLength: 1, maxLength: 20 }),
      password: fc.string({ minLength: 8, maxLength: 50 }),
      tld: fc.string({ minLength: 2, maxLength: 10 }).map(s => '.' + s)
    });

    test('DNS credentials round-trip correctly', async () => {
      await fc.assert(
        fc.asyncProperty(
          credentialsArbitrary(),
          async (credentials) => {
            // Save credentials
            const saveResult = await manager.saveDNSCredentials(credentials, testDir);
            if (!saveResult.success) return true; // Skip if save failed
            
            // Load credentials
            const loadResult = await manager.loadDNSCredentials(testDir);
            if (!loadResult.exists) return false;
            
            // Verify all fields are preserved
            const loaded = loadResult.credentials;
            return (
              loaded.server === credentials.server &&
              loaded.username === credentials.username &&
              loaded.password === credentials.password &&
              loaded.tld === credentials.tld
            );
          }
        ),
        { numRuns: 100 }
      );
    });

    test('saved credentials always include timestamp', async () => {
      await fc.assert(
        fc.asyncProperty(
          credentialsArbitrary(),
          async (credentials) => {
            await manager.saveDNSCredentials(credentials, testDir);
            const result = await manager.loadDNSCredentials(testDir);
            
            if (!result.exists) return true; // Skip if load failed
            
            return typeof result.credentials.savedAt === 'string';
          }
        ),
        { numRuns: 100 }
      );
    });
  });

  /**
   * Feature: dashcaddy-installer, Property 6: Disk Space Verification
   * For any valid path, the installer should correctly determine available disk space
   * and report consistent results across multiple checks.
   * Validates: Requirements 2.4
   */
  describe('Property 6: Disk Space Verification', () => {
    test('getDiskSpace returns consistent structure', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('\0') && !s.includes('|')),
          async (pathSegment) => {
            const testPath = path.join(testDir, pathSegment);
            const result = await manager.getDiskSpace(testPath);

            return (
              typeof result === 'object' &&
              typeof result.available === 'boolean' &&
              (result.available ? typeof result.path === 'string' : typeof result.error === 'string')
            );
          }
        ),
        { numRuns: 50 }
      );
    });

    test('getDiskSpace is idempotent for same path', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('\0') && !s.includes('|')),
          async (pathSegment) => {
            const testPath = path.join(testDir, pathSegment);

            // Check twice
            const result1 = await manager.getDiskSpace(testPath);
            const result2 = await manager.getDiskSpace(testPath);

            // Results should be consistent
            return result1.available === result2.available;
          }
        ),
        { numRuns: 30 }
      );
    });

    test('getDiskSpace returns available=true for existing writable paths', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 15 }).filter(s => /^[a-zA-Z0-9_-]+$/.test(s)),
          async (pathSegment) => {
            const testPath = path.join(testDir, pathSegment);

            // Create the directory first
            await fs.mkdir(testPath, { recursive: true });

            const result = await manager.getDiskSpace(testPath);

            return result.available === true;
          }
        ),
        { numRuns: 30 }
      );
    });
  });

  /**
   * Feature: dashcaddy-installer, Property 7: Tier Validation
   * For any configuration, only valid tier values (basic, intermediate, advanced)
   * should be accepted and persisted correctly.
   * Validates: Requirements 3.1, 3.2, 3.3
   */
  describe('Property 7: Tier Validation', () => {
    const validTiers = ['basic', 'intermediate', 'advanced'];

    test('valid tiers are preserved in config round-trip', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.constantFrom(...validTiers),
          fc.string({ minLength: 1, maxLength: 20 }),
          async (tier, dashboardName) => {
            const config = {
              installPath: testDir,
              tier: tier,
              dashboardName: dashboardName,
              apiPort: 3001,
              dnsEnabled: false,
              tailscaleEnabled: false
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true; // Skip if save/load failed

            return result.config.tier === tier;
          }
        ),
        { numRuns: 50 }
      );
    });

    test('tier value is always one of valid options after load', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.constantFrom(...validTiers),
          async (tier) => {
            const config = {
              installPath: testDir,
              tier: tier,
              dashboardName: 'Test',
              apiPort: 3001,
              dnsEnabled: false,
              tailscaleEnabled: false
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            return validTiers.includes(result.config.tier);
          }
        ),
        { numRuns: 30 }
      );
    });

    test('tier determines expected feature availability', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.constantFrom(...validTiers),
          async (tier) => {
            const config = {
              installPath: testDir,
              tier: tier,
              dashboardName: 'Test',
              apiPort: 3001,
              dnsEnabled: tier === 'advanced',
              tailscaleEnabled: tier === 'advanced'
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            // Advanced tier should support DNS and Tailscale
            if (tier === 'advanced') {
              return result.config.dnsEnabled === true && result.config.tailscaleEnabled === true;
            }

            // Non-advanced tiers should have these disabled
            return result.config.dnsEnabled === false && result.config.tailscaleEnabled === false;
          }
        ),
        { numRuns: 30 }
      );
    });
  });

  /**
   * Additional property tests for installation management
   */
  describe('Installation Management Properties', () => {
    // Generator for valid configuration objects (reused from above)
    const configArbitrary = () => fc.record({
      installPath: fc.constant(testDir),
      tier: fc.constantFrom('basic', 'intermediate', 'advanced'),
      dashboardName: fc.string({ minLength: 1, maxLength: 50 }),
      apiPort: fc.integer({ min: 1024, max: 65535 }),
      dnsEnabled: fc.boolean(),
      tailscaleEnabled: fc.boolean()
    });

    test('installationExists is consistent with config file presence', async () => {
      await fc.assert(
        fc.asyncProperty(
          configArbitrary(),
          async (config) => {
            // Use a unique directory for this test
            const uniqueDir = path.join(testDir, `test-${Date.now()}-${Math.random()}`);
            config.installPath = uniqueDir;
            
            // Before saving, installation should not exist
            const existsBefore = await manager.installationExists(uniqueDir);
            
            // Save config
            await manager.saveConfig(config, uniqueDir);
            
            // After saving, installation should exist
            const existsAfter = await manager.installationExists(uniqueDir);
            
            return !existsBefore && existsAfter;
          }
        ),
        { numRuns: 50 }
      );
    });

    test('removeInstallation removes all files', async () => {
      await fc.assert(
        fc.asyncProperty(
          configArbitrary(),
          async (config) => {
            // Create installation
            await manager.createDirectories(testDir);
            await manager.saveConfig(config, testDir);
            
            // Verify it exists
            const existsBefore = await manager.installationExists(testDir);
            if (!existsBefore) return true; // Skip if creation failed
            
            // Remove installation
            const removeResult = await manager.removeInstallation(testDir);
            if (!removeResult.success) return true; // Skip if removal failed
            
            // Verify it no longer exists
            const existsAfter = await manager.installationExists(testDir);
            
            return !existsAfter;
          }
        ),
        { numRuns: 30 }
      );
    });

    test('listInstallationFiles returns array', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.string({ minLength: 1, maxLength: 20 }),
          async (pathSegment) => {
            const installPath = path.join(testDir, pathSegment);
            const files = await manager.listInstallationFiles(installPath);

            return Array.isArray(files);
          }
        ),
        { numRuns: 50 }
      );
    });
  });

  /**
   * Configuration Edge Cases
   * Tests for special characters, boundary conditions, and data integrity
   */
  describe('Configuration Edge Cases', () => {
    test('special characters in dashboardName are preserved', async () => {
      // Generator for strings with special characters
      const specialCharArbitrary = fc.string({
        unit: fc.constantFrom(
          'a', 'Z', '0', ' ', '-', '_', '.', '!', '@', '#',
          'é', 'ñ', '中', '日', '한', 'α', 'β'
        ),
        minLength: 1,
        maxLength: 30
      });

      await fc.assert(
        fc.asyncProperty(
          specialCharArbitrary,
          async (dashboardName) => {
            const config = {
              installPath: testDir,
              tier: 'basic',
              dashboardName: dashboardName,
              apiPort: 3001,
              dnsEnabled: false,
              tailscaleEnabled: false
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            return result.config.dashboardName === dashboardName;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('port values at boundaries are handled correctly', async () => {
      const boundaryPorts = fc.constantFrom(1024, 1025, 3000, 3001, 8080, 49151, 65534, 65535);

      await fc.assert(
        fc.asyncProperty(
          boundaryPorts,
          async (port) => {
            const config = {
              installPath: testDir,
              tier: 'basic',
              dashboardName: 'Test',
              apiPort: port,
              dnsEnabled: false,
              tailscaleEnabled: false
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            return result.config.apiPort === port;
          }
        ),
        { numRuns: 20 }
      );
    });

    test('customTLD with various formats preserved correctly', async () => {
      const tldArbitrary = fc.oneof(
        fc.constant(undefined),
        fc.constant(null),
        fc.string({ minLength: 2, maxLength: 10 }).map(s => '.' + s.replace(/[^a-zA-Z]/g, 'x')),
        fc.constantFrom('.local', '.home', '.lan', '.sami', '.test')
      );

      await fc.assert(
        fc.asyncProperty(
          tldArbitrary,
          async (customTLD) => {
            const config = {
              installPath: testDir,
              tier: 'basic',
              dashboardName: 'Test',
              customTLD: customTLD,
              apiPort: 3001,
              dnsEnabled: false,
              tailscaleEnabled: false
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            // Both null/undefined should be equivalent after round-trip
            if (customTLD === null || customTLD === undefined) {
              return result.config.customTLD === null ||
                     result.config.customTLD === undefined ||
                     result.config.customTLD === customTLD;
            }

            return result.config.customTLD === customTLD;
          }
        ),
        { numRuns: 50 }
      );
    });

    test('boolean fields remain booleans after round-trip', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.boolean(),
          fc.boolean(),
          async (dnsEnabled, tailscaleEnabled) => {
            const config = {
              installPath: testDir,
              tier: 'basic',
              dashboardName: 'Test',
              apiPort: 3001,
              dnsEnabled: dnsEnabled,
              tailscaleEnabled: tailscaleEnabled
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            return (
              typeof result.config.dnsEnabled === 'boolean' &&
              typeof result.config.tailscaleEnabled === 'boolean' &&
              result.config.dnsEnabled === dnsEnabled &&
              result.config.tailscaleEnabled === tailscaleEnabled
            );
          }
        ),
        { numRuns: 50 }
      );
    });

    test('config version is valid semver format', async () => {
      const semverRegex = /^\d+\.\d+\.\d+$/;

      await fc.assert(
        fc.asyncProperty(
          fc.constantFrom('basic', 'intermediate', 'advanced'),
          async (tier) => {
            const config = {
              installPath: testDir,
              tier: tier,
              dashboardName: 'Test',
              apiPort: 3001,
              dnsEnabled: false,
              tailscaleEnabled: false
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            return semverRegex.test(result.config.version);
          }
        ),
        { numRuns: 30 }
      );
    });

    test('lastModified is valid ISO date string', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.constantFrom('basic', 'intermediate', 'advanced'),
          async (tier) => {
            const config = {
              installPath: testDir,
              tier: tier,
              dashboardName: 'Test',
              apiPort: 3001,
              dnsEnabled: false,
              tailscaleEnabled: false
            };

            await manager.saveConfig(config, testDir);
            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            // Should be parseable as a date and not NaN
            const date = new Date(result.config.lastModified);
            return !isNaN(date.getTime());
          }
        ),
        { numRuns: 30 }
      );
    });

    test('multiple rapid saves preserve final state', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 2, maxLength: 5 }),
          async (dashboardNames) => {
            // Rapidly save multiple configs
            for (const name of dashboardNames) {
              await manager.saveConfig({
                installPath: testDir,
                tier: 'basic',
                dashboardName: name,
                apiPort: 3001,
                dnsEnabled: false,
                tailscaleEnabled: false
              }, testDir);
            }

            const result = await manager.loadConfig(testDir);

            if (!result.exists) return true;

            // Final saved name should be the last one
            return result.config.dashboardName === dashboardNames[dashboardNames.length - 1];
          }
        ),
        { numRuns: 30 }
      );
    });
  });

  /**
   * DNS Credential Edge Cases
   */
  describe('DNS Credential Edge Cases', () => {
    test('passwords with special characters are preserved', async () => {
      const specialPasswordArbitrary = fc.string({
        unit: fc.constantFrom(
          'a', 'Z', '0', '!', '@', '#', '$', '%', '^', '&', '*',
          '(', ')', '-', '_', '=', '+', '[', ']', '{', '}', '|',
          ';', ':', "'", '"', '<', '>', ',', '.', '/', '?', '`', '~'
        ),
        minLength: 8,
        maxLength: 50
      });

      await fc.assert(
        fc.asyncProperty(
          specialPasswordArbitrary,
          async (password) => {
            const credentials = {
              server: '192.168.1.1',
              username: 'admin',
              password: password,
              tld: '.local'
            };

            await manager.saveDNSCredentials(credentials, testDir);
            const result = await manager.loadDNSCredentials(testDir);

            if (!result.exists) return true;

            return result.credentials.password === password;
          }
        ),
        { numRuns: 100 }
      );
    });

    test('various IP address formats in server field', async () => {
      await fc.assert(
        fc.asyncProperty(
          fc.ipV4(),
          async (server) => {
            const credentials = {
              server: server,
              username: 'admin',
              password: 'password123',
              tld: '.local'
            };

            await manager.saveDNSCredentials(credentials, testDir);
            const result = await manager.loadDNSCredentials(testDir);

            if (!result.exists) return true;

            return result.credentials.server === server;
          }
        ),
        { numRuns: 50 }
      );
    });

    test('username with edge case lengths', async () => {
      const usernameArbitrary = fc.string({
        unit: fc.constantFrom('a', 'b', 'c', '1', '2', '_', '-'),
        minLength: 1,
        maxLength: 100
      });

      await fc.assert(
        fc.asyncProperty(
          usernameArbitrary,
          async (username) => {
            const credentials = {
              server: '192.168.1.1',
              username: username,
              password: 'password123',
              tld: '.local'
            };

            await manager.saveDNSCredentials(credentials, testDir);
            const result = await manager.loadDNSCredentials(testDir);

            if (!result.exists) return true;

            return result.credentials.username === username;
          }
        ),
        { numRuns: 50 }
      );
    });
  });
});
