/**
 * Variant Port Allocation Tests
 *
 * Tests for port allocation logic in variant-config-adapter.ts.
 * Verifies unique port assignment (8318-8417) for CLIProxy variants.
 */

const assert = require('assert');
const fs = require('fs');
const path = require('path');
const os = require('os');

// Set test isolation environment before importing
const testHome = path.join(
  os.tmpdir(),
  `ccs-test-port-${Date.now()}-${Math.random().toString(36).slice(2)}`
);
process.env.CCS_HOME = testHome;

const {
  getNextAvailablePort,
  VARIANT_PORT_BASE,
  VARIANT_PORT_MAX_OFFSET,
  listVariantsFromConfig,
  saveVariantLegacy,
  removeVariantFromLegacyConfig,
} = require('../../../dist/cliproxy/services/variant-config-adapter');
const { CLIPROXY_DEFAULT_PORT } = require('../../../dist/cliproxy/config-generator');

describe('Variant Port Allocation', function () {
  let configPath;

  beforeEach(function () {
    // Create test directories
    const ccsDir = path.join(testHome, '.ccs');
    fs.mkdirSync(ccsDir, { recursive: true });
    configPath = path.join(ccsDir, 'config.json');

    // Start with empty config
    fs.writeFileSync(configPath, JSON.stringify({ profiles: {} }));
  });

  afterEach(function () {
    // Clean up config file
    if (fs.existsSync(configPath)) {
      fs.unlinkSync(configPath);
    }
  });

  afterAll(function () {
    // Clean up test directory
    try {
      fs.rmSync(testHome, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors
    }
    delete process.env.CCS_HOME;
  });

  describe('Constants', function () {
    it('VARIANT_PORT_BASE equals CLIPROXY_DEFAULT_PORT + 1 (8318)', function () {
      assert.strictEqual(VARIANT_PORT_BASE, CLIPROXY_DEFAULT_PORT + 1);
      assert.strictEqual(VARIANT_PORT_BASE, 8318);
    });

    it('VARIANT_PORT_MAX_OFFSET equals 100 (ports 8318-8417)', function () {
      assert.strictEqual(VARIANT_PORT_MAX_OFFSET, 100);
    });
  });

  describe('getNextAvailablePort - Basic Allocation', function () {
    it('returns VARIANT_PORT_BASE (8318) when no variants exist', function () {
      const port = getNextAvailablePort();
      assert.strictEqual(port, VARIANT_PORT_BASE);
      assert.strictEqual(port, 8318);
    });

    it('returns next available port when some ports used', function () {
      // Create variant on first port
      const settingsPath = path.join(testHome, '.ccs', 'test1.settings.json');
      fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));
      saveVariantLegacy('test1', 'gemini', settingsPath, undefined, 8318);

      const port = getNextAvailablePort();
      assert.strictEqual(port, 8319);
    });

    it('skips used ports and returns first gap', function () {
      const ccsDir = path.join(testHome, '.ccs');

      // Create variants on ports 8318 and 8320 (leaving 8319 as gap)
      const settingsPath1 = path.join(ccsDir, 'test1.settings.json');
      const settingsPath2 = path.join(ccsDir, 'test2.settings.json');
      fs.writeFileSync(settingsPath1, JSON.stringify({ env: {} }));
      fs.writeFileSync(settingsPath2, JSON.stringify({ env: {} }));

      saveVariantLegacy('test1', 'gemini', settingsPath1, undefined, 8318);
      saveVariantLegacy('test2', 'gemini', settingsPath2, undefined, 8320);

      // Should return 8319 (the gap)
      const port = getNextAvailablePort();
      assert.strictEqual(port, 8319);
    });

    it('allocates sequential ports for multiple variants', function () {
      const ccsDir = path.join(testHome, '.ccs');

      for (let i = 0; i < 5; i++) {
        const settingsPath = path.join(ccsDir, `variant${i}.settings.json`);
        fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));

        const port = getNextAvailablePort();
        assert.strictEqual(port, 8318 + i);

        saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, port);
      }
    });
  });

  describe('getNextAvailablePort - Boundary Conditions', function () {
    it('returns last port (8417) when 99 ports used', function () {
      const ccsDir = path.join(testHome, '.ccs');

      // Create 99 variants (ports 8318-8416)
      for (let i = 0; i < 99; i++) {
        const settingsPath = path.join(ccsDir, `variant${i}.settings.json`);
        fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));
        saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, 8318 + i);
      }

      const port = getNextAvailablePort();
      assert.strictEqual(port, 8417); // Last available port
    });

    it('throws when all 100 ports exhausted', function () {
      const ccsDir = path.join(testHome, '.ccs');

      // Create 100 variants (ports 8318-8417)
      for (let i = 0; i < 100; i++) {
        const settingsPath = path.join(ccsDir, `variant${i}.settings.json`);
        fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));
        saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, 8318 + i);
      }

      assert.throws(
        () => getNextAvailablePort(),
        /Port limit reached/,
        'Should throw error when all ports exhausted'
      );
    });

    it('error message includes variant count and recovery hint', function () {
      const ccsDir = path.join(testHome, '.ccs');

      // Create 100 variants
      for (let i = 0; i < 100; i++) {
        const settingsPath = path.join(ccsDir, `variant${i}.settings.json`);
        fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));
        saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, 8318 + i);
      }

      try {
        getNextAvailablePort();
        assert.fail('Should have thrown error');
      } catch (err) {
        assert.ok(err.message.includes('100/100'), 'Should include variant count');
        assert.ok(
          err.message.includes('ccs cliproxy remove'),
          'Should include recovery hint'
        );
      }
    });
  });

  describe('getNextAvailablePort - Port Reuse', function () {
    it('reuses port after variant deletion', function () {
      const ccsDir = path.join(testHome, '.ccs');

      // Create variant on port 8318
      const settingsPath = path.join(ccsDir, 'test.settings.json');
      fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));
      saveVariantLegacy('test', 'gemini', settingsPath, undefined, 8318);

      // Next port should be 8319
      let nextPort = getNextAvailablePort();
      assert.strictEqual(nextPort, 8319);

      // Remove the variant
      removeVariantFromLegacyConfig('test');

      // Now 8318 should be available again
      nextPort = getNextAvailablePort();
      assert.strictEqual(nextPort, 8318);
    });

    it('allocates lowest available port (not most recently freed)', function () {
      const ccsDir = path.join(testHome, '.ccs');

      // Create 3 variants on ports 8318, 8319, 8320
      for (let i = 0; i < 3; i++) {
        const settingsPath = path.join(ccsDir, `variant${i}.settings.json`);
        fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));
        saveVariantLegacy(`variant${i}`, 'gemini', settingsPath, undefined, 8318 + i);
      }

      // Remove middle variant (port 8319)
      removeVariantFromLegacyConfig('variant1');

      // Next allocation should use 8319 (the gap), not 8321
      const nextPort = getNextAvailablePort();
      assert.strictEqual(nextPort, 8319);
    });
  });

  describe('getNextAvailablePort - Legacy Variant Handling', function () {
    it('ignores variants without port field in usage calculation', function () {
      // Create variant without port (legacy format)
      const config = {
        profiles: {},
        cliproxy: {
          legacy_variant: {
            provider: 'gemini',
            settings: '/path/to/settings.json',
            // No port field
          },
        },
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Should return first port since legacy variant has no port
      const port = getNextAvailablePort();
      assert.strictEqual(port, 8318);
    });

    it('handles mixed legacy and modern variants', function () {
      const ccsDir = path.join(testHome, '.ccs');

      // Create legacy variant without port
      const config = {
        profiles: {},
        cliproxy: {
          legacy: {
            provider: 'gemini',
            settings: path.join(ccsDir, 'legacy.settings.json'),
            // No port field
          },
          modern: {
            provider: 'gemini',
            settings: path.join(ccsDir, 'modern.settings.json'),
            port: 8318,
          },
        },
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      // Should skip 8318 (used by modern) and return 8319
      const port = getNextAvailablePort();
      assert.strictEqual(port, 8319);
    });
  });

  describe('listVariantsFromConfig', function () {
    it('returns empty object when no variants exist', function () {
      const variants = listVariantsFromConfig();
      assert.deepStrictEqual(variants, {});
    });

    it('includes port field for each variant', function () {
      const ccsDir = path.join(testHome, '.ccs');
      const settingsPath = path.join(ccsDir, 'test.settings.json');
      fs.writeFileSync(settingsPath, JSON.stringify({ env: {} }));

      saveVariantLegacy('test', 'gemini', settingsPath, undefined, 8318);

      const variants = listVariantsFromConfig();
      assert.strictEqual(variants.test.port, 8318);
      assert.strictEqual(variants.test.provider, 'gemini');
    });

    it('returns undefined port for legacy variants', function () {
      // Create legacy variant without port
      const config = {
        profiles: {},
        cliproxy: {
          legacy: {
            provider: 'gemini',
            settings: '/path/to/settings.json',
            // No port field
          },
        },
      };
      fs.writeFileSync(configPath, JSON.stringify(config));

      const variants = listVariantsFromConfig();
      assert.strictEqual(variants.legacy.port, undefined);
      assert.strictEqual(variants.legacy.provider, 'gemini');
    });
  });
});
