/**
 * Tests for CLIProxy Config Generator
 * Verifies config.yaml generation including Windows path handling
 */

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

// Mock the config-manager module to provide test paths
const originalModule = require.cache[require.resolve('../../../dist/utils/config-manager')];

describe('Config Generator', () => {
  let configGenerator;
  let mockCcsDir;

  beforeEach(() => {
    // Clear the require cache to reload module with fresh mocks
    delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];

    // Set up a temporary test directory as CCS_HOME
    mockCcsDir = '/test/home/.ccs';
  });

  describe('generateUnifiedConfigContent', () => {
    it('converts Windows backslashes to forward slashes in auth-dir path', () => {
      // Simulate Windows path with backslashes
      const windowsPath = 'C:\\Users\\TestUser\\.ccs\\cliproxy\\auth';
      const normalizedPath = windowsPath.replace(/\\/g, '/');

      // Verify the replacement works correctly
      assert.strictEqual(normalizedPath, 'C:/Users/TestUser/.ccs/cliproxy/auth');
      assert(!normalizedPath.includes('\\'), 'Path should not contain backslashes');
    });

    it('handles mixed path separators', () => {
      // Test with mixed Windows and forward slashes
      const mixedPath = 'C:\\Users/TestUser\\.ccs/cliproxy\\auth';
      const normalizedPath = mixedPath.replace(/\\/g, '/');

      assert.strictEqual(normalizedPath, 'C:/Users/TestUser/.ccs/cliproxy/auth');
    });

    it('leaves forward slashes unchanged', () => {
      // Test with Unix paths (forward slashes only)
      const unixPath = '/home/testuser/.ccs/cliproxy/auth';
      const normalizedPath = unixPath.replace(/\\/g, '/');

      assert.strictEqual(normalizedPath, '/home/testuser/.ccs/cliproxy/auth');
    });

    it('generates valid YAML with normalized paths', () => {
      // Test the actual pattern used in config-generator.ts line 130
      const testAuthDir = 'C:\\Users\\TestUser\\.ccs\\cliproxy\\auth';
      const configLine = `auth-dir: "${testAuthDir.replace(/\\/g, '/')}"`;

      // Verify the YAML line is properly formatted
      assert(configLine.includes('auth-dir:'), 'Should contain auth-dir key');
      assert(configLine.includes('C:/Users/TestUser'), 'Should have normalized path');
      assert(!configLine.includes('\\'), 'Should not contain backslashes');

      // Verify it's valid YAML format
      assert.strictEqual(configLine, 'auth-dir: "C:/Users/TestUser/.ccs/cliproxy/auth"');
    });

    it('handles multiple consecutive backslashes', () => {
      // Edge case: multiple backslashes
      const weirdPath = 'C:\\\\Users\\\\TestUser\\\\.ccs\\\\cliproxy\\\\auth';
      const normalizedPath = weirdPath.replace(/\\/g, '/');

      assert.strictEqual(normalizedPath, 'C://Users//TestUser//.ccs//cliproxy//auth');
      assert(!normalizedPath.includes('\\'), 'Should not contain backslashes');
    });

    it('preserves all other characters in path', () => {
      // Test that normalization doesn't affect other characters
      const complexPath = 'D:\\Projects\\ccs-2024\\test\\.ccs\\auth-tokens\\provider.json';
      const normalizedPath = complexPath.replace(/\\/g, '/');

      assert.strictEqual(
        normalizedPath,
        'D:/Projects/ccs-2024/test/.ccs/auth-tokens/provider.json'
      );
      assert(normalizedPath.includes('Projects'), 'Should preserve directory names');
      assert(normalizedPath.includes('auth-tokens'), 'Should preserve hyphens');
      assert(normalizedPath.includes('provider.json'), 'Should preserve filenames');
    });

    it('works with environment variable expansion', () => {
      // Simulate path with environment variables
      const envPath = process.env.USERPROFILE || 'C:\\Users\\TestUser';
      const fullPath = path.join(envPath, '.ccs', 'cliproxy', 'auth');
      const normalizedPath = fullPath.replace(/\\/g, '/');

      // On Windows, path.join creates backslashes, on Unix forward slashes
      // The normalization should ensure forward slashes regardless
      assert(!normalizedPath.includes('\\'), 'Should not contain backslashes after normalization');
      assert(normalizedPath.includes('/.ccs/'), 'Should have normalized CCS path');
    });

    it('is idempotent - applying normalization twice gives same result', () => {
      const windowsPath = 'C:\\Users\\Test\\.ccs\\auth';
      const normalized1 = windowsPath.replace(/\\/g, '/');
      const normalized2 = normalized1.replace(/\\/g, '/');

      assert.strictEqual(normalized1, normalized2, 'Should be idempotent');
      assert.strictEqual(normalized2, 'C:/Users/Test/.ccs/auth');
    });

    it('handles YAML escaping requirements correctly', () => {
      // Windows paths with backslashes can cause YAML parsing issues
      // because backslash is an escape character in YAML strings
      const windowsPath = 'C:\\Users\\test\\.ccs\\auth';

      // Before fix: would contain backslashes, causing YAML parsing errors
      // After fix: forward slashes, which are safe in YAML
      const yamlUnsafePath = `auth-dir: "${windowsPath}"`;
      const yamlSafePath = `auth-dir: "${windowsPath.replace(/\\/g, '/')}"`;

      // The fixed version should be YAML-safe (no escape character conflicts)
      assert(yamlSafePath.includes('/'), 'Fixed path should use forward slashes');
      assert.strictEqual(yamlSafePath, 'auth-dir: "C:/Users/test/.ccs/auth"');
    });
  });

  describe('Path separator consistency', () => {
    it('ensures forward slashes work across all platforms', () => {
      // Forward slashes work on Windows, macOS, and Linux
      const forwardSlashPath = '/home/user/.ccs/cliproxy/auth';

      // This should work everywhere
      assert.strictEqual(
        forwardSlashPath.replace(/\\/g, '/'),
        '/home/user/.ccs/cliproxy/auth',
        'Forward slashes should be preserved on Unix'
      );

      const windowsPath = 'C:\\Users\\user\\.ccs\\cliproxy\\auth';
      const normalized = windowsPath.replace(/\\/g, '/');
      assert.strictEqual(
        normalized,
        'C:/Users/user/.ccs/cliproxy/auth',
        'Windows paths should be normalized to forward slashes'
      );

      // Both formats should be usable with Node.js fs and path modules
      // and CLIProxyAPI's path handling
    });

    it('avoids YAML escape sequence issues', () => {
      // Backslashes in YAML can cause issues like:
      // - \n being interpreted as newline
      // - \t being interpreted as tab
      // - \u being interpreted as unicode
      // Using forward slashes avoids all these issues

      const problematicPath = 'C:\\Users\\test\\.ccs\\auth';
      const safePath = problematicPath.replace(/\\/g, '/');

      // Check for problematic escape sequences
      assert(!safePath.includes('\\n'), 'Should not create \\n sequences');
      assert(!safePath.includes('\\t'), 'Should not create \\t sequences');
      assert(!safePath.includes('\\u'), 'Should not create \\u sequences');

      // The safe path should be suitable for YAML
      assert.strictEqual(safePath, 'C:/Users/test/.ccs/auth');
    });
  });

  // =========================================================================
  // API Key Preservation Tests (Issue #200)
  // =========================================================================
  describe('parseUserApiKeys', () => {
    let parseUserApiKeys;

    beforeEach(() => {
      // Clear cache and reload module
      delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
      const configGenerator = require('../../../dist/cliproxy/config-generator');
      parseUserApiKeys = configGenerator.parseUserApiKeys;
    });

    it('extracts user-added API keys from config content', () => {
      const configContent = `
# CLIProxyAPI config generated by CCS v4
port: 8317

api-keys:
  - "ccs-internal-managed"
  - "user-custom-key-12345"
  - "another-user-key-abcde"

auth-dir: "/home/user/.ccs/cliproxy/auth"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, ['user-custom-key-12345', 'another-user-key-abcde']);
    });

    it('excludes the internal CCS key', () => {
      const configContent = `
api-keys:
  - "ccs-internal-managed"
  - "my-custom-key"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.strictEqual(userKeys.length, 1);
      assert.strictEqual(userKeys[0], 'my-custom-key');
      assert(!userKeys.includes('ccs-internal-managed'), 'Should not include internal key');
    });

    it('returns empty array when only internal key exists', () => {
      const configContent = `
api-keys:
  - "ccs-internal-managed"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, []);
    });

    it('returns empty array when no api-keys section exists', () => {
      const configContent = `
port: 8317
debug: false
auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, []);
    });

    it('handles single user key', () => {
      const configContent = `
api-keys:
  - "ccs-internal-managed"
  - "single-user-key"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, ['single-user-key']);
    });

    it('handles multiple user keys', () => {
      const configContent = `
api-keys:
  - "ccs-internal-managed"
  - "key1"
  - "key2"
  - "key3"
  - "key4"
  - "key5"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, ['key1', 'key2', 'key3', 'key4', 'key5']);
    });

    it('handles keys with special characters', () => {
      const configContent = `
api-keys:
  - "ccs-internal-managed"
  - "sk_live_1234567890abcdef"
  - "api-key-with-dashes"
  - "key_with_underscores_123"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, [
        'sk_live_1234567890abcdef',
        'api-key-with-dashes',
        'key_with_underscores_123',
      ]);
    });

    it('preserves key order', () => {
      const configContent = `
api-keys:
  - "ccs-internal-managed"
  - "zebra-key"
  - "alpha-key"
  - "middle-key"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, ['zebra-key', 'alpha-key', 'middle-key']);
    });

    it('handles empty string key (edge case)', () => {
      const configContent = `
api-keys:
  - "ccs-internal-managed"
  - ""
  - "valid-key"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      // Empty strings should be filtered out - parseUserApiKeys checks: key && key !== CCS_INTERNAL_API_KEY
      // Empty string is falsy, so it's excluded along with internal key
      assert.strictEqual(userKeys.length, 1, 'Should filter out empty string key');
      assert.deepStrictEqual(userKeys, ['valid-key'], 'Should only include valid non-empty keys');
    });

    it('handles config with Windows line endings', () => {
      const configContent =
        'api-keys:\r\n  - "ccs-internal-managed"\r\n  - "user-key"\r\n\r\nauth-dir: "/test"\r\n';

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, ['user-key']);
    });

    it('handles different indentation styles', () => {
      const configContent = `
api-keys:
    - "ccs-internal-managed"
    - "user-key-with-4-spaces"

auth-dir: "/test"
`;

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, ['user-key-with-4-spaces']);
    });

    it('handles tabs in indentation', () => {
      const configContent =
        'api-keys:\n\t- "ccs-internal-managed"\n\t- "user-key-with-tab"\n\nauth-dir: "/test"';

      const userKeys = parseUserApiKeys(configContent);

      assert.deepStrictEqual(userKeys, ['user-key-with-tab']);
    });
  });

  describe('regenerateConfig API key preservation', () => {
    const fs = require('fs');
    const os = require('os');
    const path = require('path');

    let testDir;
    let originalCcsHome;
    let regenerateConfig;
    let getCliproxyConfigPath;

    beforeEach(() => {
      // Create a temporary test directory
      testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-'));
      originalCcsHome = process.env.CCS_HOME;
      process.env.CCS_HOME = testDir;

      // Clear cache and reload module
      delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
      delete require.cache[require.resolve('../../../dist/utils/config-manager')];
      const configGenerator = require('../../../dist/cliproxy/config-generator');
      regenerateConfig = configGenerator.regenerateConfig;
      getCliproxyConfigPath = configGenerator.getCliproxyConfigPath;
    });

    afterEach(() => {
      // Restore environment
      process.env.CCS_HOME = originalCcsHome;

      // Clean up test directory
      if (testDir && fs.existsSync(testDir)) {
        fs.rmSync(testDir, { recursive: true, force: true });
      }
    });

    it('preserves user API keys during regeneration', () => {
      // Create initial config with user keys
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v3
port: 8317

api-keys:
  - "ccs-internal-managed"
  - "user-custom-key-12345"
  - "another-user-key"

auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      // Regenerate config (simulating CCS update)
      regenerateConfig();

      // Read regenerated config
      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');

      // Verify user keys are preserved
      assert(newConfig.includes('user-custom-key-12345'), 'Should preserve first user key');
      assert(newConfig.includes('another-user-key'), 'Should preserve second user key');
      assert(newConfig.includes('ccs-internal-managed'), 'Should include internal key');
    });

    it('preserves port setting during regeneration', () => {
      // Create initial config with custom port
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v3
port: 9999

api-keys:
  - "ccs-internal-managed"

auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      // Regenerate config
      regenerateConfig();

      // Read regenerated config
      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');

      // Verify port is preserved
      assert(newConfig.includes('port: 9999'), 'Should preserve custom port');
    });

    it('preserves openai-compatibility connectors during regeneration', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v17
port: 8317

api-keys:
  - "ccs-internal-managed"

auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"

openai-compatibility:
  - name: mimo
    base-url: https://api.xiaomimimo.com/v1
    api-key-entries:
      - api-key: sk-test
    models:
      - name: mimo-v2-flash
        alias: mimo-v2-flash
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(newConfig.includes('openai-compatibility:'), 'Should preserve openai-compatibility');
      assert(newConfig.includes('name: mimo'), 'Should preserve connector name');
      assert(newConfig.includes('base-url: https://api.xiaomimimo.com/v1'), 'Should preserve base URL');
      assert(newConfig.includes('alias: mimo-v2-flash'), 'Should preserve model aliases');
    });

    it('creates fresh config when none exists', () => {
      // Ensure clean state
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      const configPath = path.join(cliproxyDir, 'config.yaml');

      assert(!fs.existsSync(configPath), 'Config should not exist initially');

      // Generate config
      regenerateConfig();

      // Verify config was created
      assert(fs.existsSync(configPath), 'Config should be created');

      const config = fs.readFileSync(configPath, 'utf-8');
      assert(config.includes('ccs-internal-managed'), 'Should include internal key');
      assert(config.includes('port: 8317'), 'Should use default port');
    });

    it('handles corrupted config gracefully', () => {
      // Create corrupted config
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), 'invalid yaml content: [[[');

      // Should not throw
      assert.doesNotThrow(() => regenerateConfig());

      // Should create valid config
      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(newConfig.includes('ccs-internal-managed'), 'Should create valid config');
    });

    it('handles empty config file gracefully', () => {
      // Create empty config
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), '');

      // Should not throw
      assert.doesNotThrow(() => regenerateConfig());

      // Should create valid config with defaults
      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(newConfig.includes('ccs-internal-managed'), 'Should create valid config');
      assert(newConfig.includes('port: 8317'), 'Should use default port');
    });

    it('preserves multiple user keys in correct order', () => {
      // Create config with multiple user keys
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v3
port: 8317

api-keys:
  - "ccs-internal-managed"
  - "key-first"
  - "key-second"
  - "key-third"

auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      // Regenerate config
      regenerateConfig();

      // Read regenerated config
      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');

      // Verify order is preserved (first > second > third)
      const firstPos = newConfig.indexOf('key-first');
      const secondPos = newConfig.indexOf('key-second');
      const thirdPos = newConfig.indexOf('key-third');

      assert(firstPos < secondPos, 'First key should come before second');
      assert(secondPos < thirdPos, 'Second key should come before third');
    });
  });

  describe('Cross-platform path handling', () => {
    it('normalizes paths regardless of current platform', () => {
      // The fix should work consistently on Windows, macOS, and Linux

      const testPaths = [
        // Windows-style paths
        { input: 'C:\\Users\\test\\.ccs\\auth', expected: 'C:/Users/test/.ccs/auth' },
        { input: 'D:\\Projects\\ccs\\auth', expected: 'D:/Projects/ccs/auth' },
        // Unix-style paths
        { input: '/home/user/.ccs/auth', expected: '/home/user/.ccs/auth' },
        { input: '/var/ccs/auth', expected: '/var/ccs/auth' },
        // Network paths
        { input: '\\\\network\\share\\.ccs\\auth', expected: '//network/share/.ccs/auth' },
      ];

      testPaths.forEach(({ input, expected }) => {
        const normalized = input.replace(/\\/g, '/');
        assert.strictEqual(normalized, expected, `Should normalize "${input}" to "${expected}"`);
      });
    });
  });

  describe('oauth-model-alias fork:true', () => {
    const fs = require('fs');
    const os = require('os');
    const path = require('path');

    let testDir;
    let originalCcsHome;
    let regenerateConfig;

    beforeEach(() => {
      testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-fork-'));
      originalCcsHome = process.env.CCS_HOME;
      process.env.CCS_HOME = testDir;

      delete require.cache[require.resolve('../../../dist/cliproxy/config-generator')];
      delete require.cache[require.resolve('../../../dist/utils/config-manager')];
      const configGenerator = require('../../../dist/cliproxy/config-generator');
      regenerateConfig = configGenerator.regenerateConfig;
    });

    afterEach(() => {
      process.env.CCS_HOME = originalCcsHome;
      if (testDir && fs.existsSync(testDir)) {
        fs.rmSync(testDir, { recursive: true, force: true });
      }
    });

    it('generates fork:true for Claude model aliases', () => {
      regenerateConfig();

      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');

      // Claude aliases should have fork: true
      assert(config.includes('claude-sonnet-4-6'), 'Should include Claude Sonnet 4.6 model');
      assert(config.includes('fork: true'), 'Should include fork: true for Claude aliases');
      assert(
        !config.includes('claude-sonnet-4-5'),
        'Should not include denylisted Claude Sonnet 4.5 aliases'
      );
      assert(
        !config.includes('claude-opus-4-5'),
        'Should not include denylisted Claude Opus 4.5 aliases'
      );
      assert(
        !config.includes('alias: gemini-claude-'),
        'Should not emit deprecated gemini-claude aliases'
      );

      // Verify fork: true appears after each Claude alias entry
      const lines = config.split('\n');
      for (let i = 0; i < lines.length; i++) {
        if (lines[i].includes('alias: claude-')) {
          assert(
            lines[i + 1] && lines[i + 1].trim() === 'fork: true',
            `fork: true should follow Claude alias at line ${i}: ${lines[i]}`
          );
        }
      }
    });

    it('does not generate fork:true for non-Claude aliases', () => {
      regenerateConfig();

      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');

      // Gemini aliases should NOT have fork: true
      const lines = config.split('\n');
      for (let i = 0; i < lines.length; i++) {
        if (lines[i].includes('alias: gemini-3-') || lines[i].includes('alias: gemini-2.5-')) {
          const nextLine = lines[i + 1] || '';
          assert(
            !nextLine.trim().startsWith('fork:'),
            `Gemini alias should not have fork: ${lines[i]}`
          );
        }
      }
    });

    it('generates Gemini 3.1 compatibility aliases without fork', () => {
      regenerateConfig();

      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      const lines = config.split('\n');

      const gemini31AliasLines = [
        'alias: gemini-3.1-pro-preview',
        'alias: gemini-3.1-pro-preview-customtools',
        'alias: gemini-3.1-flash-preview',
        'alias: gemini-3.1-flash-preview-customtools',
      ];

      for (const aliasLine of gemini31AliasLines) {
        const lineIndex = lines.findIndex((line) => line.includes(aliasLine));
        assert(lineIndex >= 0, `Should include ${aliasLine}`);
        const nextLine = lines[lineIndex + 1] || '';
        assert(
          !nextLine.trim().startsWith('fork:'),
          `Gemini alias should not have fork: ${aliasLine}`
        );
      }
    });

    it('generates dot and hyphen alias forms for preview compatibility', () => {
      regenerateConfig();

      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');

      assert(
        config.includes('alias: gemini-3.1-pro-preview'),
        'Should include dotted version alias'
      );
      assert(
        config.includes('alias: gemini-3-1-pro-preview'),
        'Should include hyphen version alias'
      );
      assert(
        config.includes('alias: gemini-3-1-pro-preview-customtools'),
        'Should include hyphen customtools alias'
      );
    });

    it('preserves multiple aliases that share the same model name', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v8
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: custom-model
      alias: custom-alias-a
    - name: custom-model
      alias: custom-alias-b
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');

      assert(newConfig.includes('alias: custom-alias-a'), 'Should preserve first alias');
      assert(newConfig.includes('alias: custom-alias-b'), 'Should preserve second alias');
      assert.strictEqual(
        (newConfig.match(/alias: custom-alias-a/g) || []).length,
        1,
        'Should keep one entry for first alias'
      );
      assert.strictEqual(
        (newConfig.match(/alias: custom-alias-b/g) || []).length,
        1,
        'Should keep one entry for second alias'
      );
    });

    it('does not auto-enrich aliases from cached catalog to avoid model list bloat', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      const cachePath = path.join(testDir, '.ccs', 'model-catalog-cache.json');
      fs.mkdirSync(path.dirname(cachePath), { recursive: true });

      regenerateConfig();

      const beforeCacheConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        !beforeCacheConfig.includes('alias: gemini-3.11-pro-preview'),
        'Should not include unseen minor alias before cache enrichment'
      );

      const cachePayload = {
        providers: {
          agy: [{ id: 'gemini-3.11-pro-preview', display_name: 'Gemini 3.11 Pro Preview' }],
        },
        fetchedAt: Date.now(),
      };
      fs.writeFileSync(cachePath, JSON.stringify(cachePayload));

      regenerateConfig();

      const afterCacheConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        !afterCacheConfig.includes('alias: gemini-3.11-pro-preview'),
        'Should not include cache-derived unseen minor alias'
      );
      assert(
        !afterCacheConfig.includes('alias: gemini-3.11-pro-preview-customtools'),
        'Should not include compatibility alias for cache-derived entry'
      );
      assert(
        afterCacheConfig.includes('alias: gemini-3-pro-preview'),
        'Should keep baseline Gemini preview alias generation'
      );
      assert(
        afterCacheConfig.includes('alias: gemini-3-pro-preview-customtools'),
        'Should keep baseline compatibility alias generation'
      );
    });

    it('drops stale multi-version Gemini preview ranges while preserving true custom aliases', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });
      const guessedMinors = ['3', '4', '5', '6'];
      const buildGuessedRange = (name, family) =>
        guessedMinors
          .flatMap((minor) => [
            `    - name: ${name}`,
            `      alias: gemini-3.${minor}-${family}-preview`,
            `    - name: ${name}`,
            `      alias: gemini-3.${minor}-${family}-preview-customtools`,
            `    - name: ${name}`,
            `      alias: gemini-3-${minor}-${family}-preview`,
            `    - name: ${name}`,
            `      alias: gemini-3-${minor}-${family}-preview-customtools`,
          ])
          .join('\n');

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
${buildGuessedRange('gemini-3-pro-high', 'pro')}
${buildGuessedRange('gemini-3-flash', 'flash')}
    - name: custom-model
      alias: keep-me
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      for (const minor of guessedMinors) {
        assert(
          !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`),
          `Should remove stale generated Pro aliases for 3.${minor}`
        );
        assert(
          !newConfig.includes(`alias: gemini-3-${minor}-flash-preview-customtools`),
          `Should remove stale generated Flash aliases for 3.${minor}`
        );
      }
      assert(newConfig.includes('alias: keep-me'), 'Should preserve true custom aliases');
      assert(
        newConfig.includes('alias: gemini-3-pro-preview'),
        'Should still keep current baseline Pro preview alias'
      );
      assert(
        newConfig.includes('alias: gemini-3.1-flash-preview-customtools'),
        'Should still keep current baseline Flash compatibility alias'
      );
    });

    it('preserves sparse manual minor-version overrides when a stale guessed range is pruned', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });
      const guessedMinors = ['3', '4', '5', '6'];
      const staleRange = guessedMinors
        .flatMap((minor) => [
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview-customtools`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3-${minor}-pro-preview`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3-${minor}-pro-preview-customtools`,
        ])
        .join('\n');

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: gemini-3-pro-high
      alias: gemini-3.2-pro-preview
${staleRange}
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview'),
        'Should preserve sparse manual minor-version override'
      );
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview-customtools'),
        'Should still expand compatibility aliases for preserved sparse manual override'
      );
      for (const minor of guessedMinors) {
        assert(
          !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`),
          `Should prune stale guessed aliases for 3.${minor}`
        );
      }
    });

    it('preserves a manual 3.2 compatibility cluster while pruning higher stale guessed minors', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });
      const guessedMinors = ['3', '4', '5', '6'];
      const staleRange = guessedMinors
        .flatMap((minor) => [
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview-customtools`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3-${minor}-pro-preview`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3-${minor}-pro-preview-customtools`,
        ])
        .join('\n');

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: gemini-3-pro-high
      alias: gemini-3.2-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3.2-pro-preview-customtools
    - name: gemini-3-pro-high
      alias: gemini-3-2-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3-2-pro-preview-customtools
${staleRange}
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview'),
        'Should preserve the manual 3.2 compatibility cluster during legacy cleanup'
      );
      assert(
        newConfig.includes('alias: gemini-3-2-pro-preview-customtools'),
        'Should preserve hyphenated manual compatibility aliases for 3.2'
      );
      for (const minor of guessedMinors) {
        assert(
          !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`),
          `Should prune higher stale guessed aliases for 3.${minor}`
        );
      }
    });

    it('prunes partially retained broad guessed ranges during legacy cleanup', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });
      const guessedMinors = ['3', '4', '5'];
      const partialRange = guessedMinors
        .flatMap((minor) => [
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview-customtools`,
        ])
        .join('\n');

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
${partialRange}
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      for (const minor of guessedMinors) {
        assert(
          !newConfig.includes(`alias: gemini-3.${minor}-pro-preview`),
          `Should prune partially retained guessed aliases for 3.${minor}`
        );
      }
    });

    it('preserves manual high-only multi-version clusters that are not broad guessed ranges', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: gemini-3-pro-high
      alias: gemini-3.3-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3.3-pro-preview-customtools
    - name: gemini-3-pro-high
      alias: gemini-3.4-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3.4-pro-preview-customtools
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: gemini-3.3-pro-preview'),
        'Should preserve manual 3.3 aliases when the cluster is not broad enough to infer stale fan-out'
      );
      assert(
        newConfig.includes('alias: gemini-3.4-pro-preview-customtools'),
        'Should preserve manual 3.4 compatibility aliases when provenance is ambiguous'
      );
    });

    it('preserves single-minor high-version aliases when they are not part of a guessed range', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: gemini-3-pro-high
      alias: gemini-3.11-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3.11-pro-preview-customtools
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: gemini-3.11-pro-preview'),
        'Should preserve lone manual high-version aliases during legacy cleanup'
      );
      assert(
        newConfig.includes('alias: gemini-3.11-pro-preview-customtools'),
        'Should still expand compatibility aliases for preserved high-version aliases'
      );
    });

    it('writes a one-time backup before pruning legacy Gemini aliases during migration', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });
      const configPath = path.join(cliproxyDir, 'config.yaml');
      const guessedMinors = ['3', '4', '5'];
      const staleRange = guessedMinors
        .flatMap((minor) => [
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview`,
          `    - name: gemini-3-pro-high`,
          `      alias: gemini-3.${minor}-pro-preview-customtools`,
        ])
        .join('\n');

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
${staleRange}
`;
      fs.writeFileSync(configPath, initialConfig);

      regenerateConfig();

      const backupPath = `${configPath}.pre-v16-gemini-alias-cleanup.bak`;
      assert(fs.existsSync(backupPath), 'Should keep a backup of the legacy config before pruning');
      assert(
        fs.readFileSync(backupPath, 'utf-8').includes('gemini-3.5-pro-preview'),
        'Should preserve the pruned legacy alias in the migration backup'
      );
    });

    it('preserves manual Gemini preview overrides on built-in alias names', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: gemini-3-pro-high
      alias: gemini-3.2-pro-preview
    - name: gemini-3-flash
      alias: gemini-3.2-flash-preview
      fork: true
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview'),
        'Should preserve manual Pro preview overrides on built-in alias names'
      );
      assert(
        newConfig.includes('alias: gemini-3.2-flash-preview'),
        'Should preserve manual Flash preview overrides on built-in alias names'
      );
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview-customtools'),
        'Should still expand compatibility aliases for preserved manual Pro override'
      );
      assert(
        newConfig.includes('alias: gemini-3.2-flash-preview-customtools'),
        'Should still expand compatibility aliases for preserved manual Flash override'
      );

      const lines = newConfig.split('\n');
      for (let i = 0; i < lines.length; i++) {
        if (lines[i].includes('alias: gemini-3.2-flash-preview')) {
          assert(
            lines[i + 1] && lines[i + 1].trim() === 'fork: true',
            'Should preserve fork: true on manual Flash preview override'
          );
          break;
        }
      }
    });

    it('preserves manual Gemini preview compatibility clusters on built-in alias names', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v12
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: gemini-3-pro-high
      alias: gemini-3.2-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3.2-pro-preview-customtools
    - name: gemini-3-pro-high
      alias: gemini-3-2-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3-2-pro-preview-customtools
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview'),
        'Should preserve manual dotted Pro preview override'
      );
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview-customtools'),
        'Should preserve manual dotted Pro customtools override'
      );
      assert(
        newConfig.includes('alias: gemini-3-2-pro-preview'),
        'Should preserve manual hyphenated Pro preview override'
      );
      assert(
        newConfig.includes('alias: gemini-3-2-pro-preview-customtools'),
        'Should preserve manual hyphenated Pro customtools override'
      );
    });

    it('does not re-prune multi-version manual clusters once the config is already migrated', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v16
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: gemini-3-pro-high
      alias: gemini-3.2-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3.3-pro-preview
    - name: gemini-3-pro-high
      alias: gemini-3.4-pro-preview
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: gemini-3.2-pro-preview'),
        'Should preserve manual aliases after legacy migration has already run'
      );
      assert(
        newConfig.includes('alias: gemini-3.3-pro-preview'),
        'Should not re-prune later manual version clusters on v16+ configs'
      );
      assert(
        newConfig.includes('alias: gemini-3.4-pro-preview'),
        'Should leave already-migrated multi-version manual aliases untouched'
      );
    });

    it('preserves user-added aliases with fork during regeneration', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v6
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: custom-model
      alias: my-custom-alias
      fork: true
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(newConfig.includes('custom-model'), 'Should preserve custom alias name');
      assert(newConfig.includes('my-custom-alias'), 'Should preserve custom alias');

      // Check fork is preserved for user alias
      const lines = newConfig.split('\n');
      for (let i = 0; i < lines.length; i++) {
        if (lines[i].includes('alias: my-custom-alias')) {
          assert(
            lines[i + 1] && lines[i + 1].trim() === 'fork: true',
            'Should preserve fork: true for user-added alias'
          );
        }
      }
    });

    it('normalizes deprecated gemini-claude aliases during regeneration', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v9
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: claude-sonnet-4-6-thinking
      alias: gemini-claude-sonnet-4-6-thinking
      fork: true
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: claude-sonnet-4-6'),
        'Should migrate deprecated sonnet thinking alias to canonical model ID'
      );
      assert(
        !newConfig.includes('alias: gemini-claude-sonnet-4-6-thinking'),
        'Should remove deprecated gemini-claude alias after regeneration'
      );
    });

    it('normalizes dotted deprecated sonnet thinking aliases during regeneration', () => {
      const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
      fs.mkdirSync(cliproxyDir, { recursive: true });

      const initialConfig = `# CLIProxyAPI config generated by CCS v11
port: 8317
api-keys:
  - "ccs-internal-managed"
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
oauth-model-alias:
  antigravity:
    - name: claude-sonnet-4-6-thinking
      alias: claude-sonnet-4.6-thinking
      fork: true
`;
      fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);

      regenerateConfig();

      const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
      assert(
        newConfig.includes('alias: claude-sonnet-4-6'),
        'Should migrate dotted deprecated sonnet thinking alias to canonical model ID'
      );
      assert(
        !newConfig.includes('alias: claude-sonnet-4.6-thinking'),
        'Should remove dotted deprecated sonnet thinking alias after regeneration'
      );
    });
  });
});
