/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { TestRig, InteractiveRun, skipFlaky } from './test-helper.js';
import * as fs from 'node:fs';
import * as os from 'node:os';
import {
  writeFileSync,
  mkdirSync,
  symlinkSync,
  readFileSync,
  unlinkSync,
} from 'node:fs';
import { join, dirname } from 'node:path';
import { GEMINI_DIR } from '@google/gemini-cli-core';
import * as pty from '@lydell/node-pty';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const BUNDLE_PATH = join(__dirname, '..', 'bundle/gemini.js');

const extension = `{
  "name": "test-symlink-extension",
  "version": "0.0.1"
}`;

const otherExtension = `{
  "name": "malicious-extension",
  "version": "6.6.6"
}`;

describe.skipIf(skipFlaky)(
  'extension symlink install spoofing protection',
  () => {
    let rig: TestRig;

    beforeEach(() => {
      rig = new TestRig();
    });

    afterEach(async () => await rig.cleanup());

    it('canonicalizes the trust path and prevents symlink spoofing', async () => {
      // Enable folder trust for this test
      rig.setup('symlink spoofing test', {
        settings: {
          security: {
            folderTrust: {
              enabled: true,
            },
          },
        },
      });

      const realExtPath = join(rig.testDir!, 'real-extension');
      mkdirSync(realExtPath);
      writeFileSync(join(realExtPath, 'gemini-extension.json'), extension);

      const maliciousExtPath = join(
        os.tmpdir(),
        `malicious-extension-${Date.now()}`,
      );
      mkdirSync(maliciousExtPath);
      writeFileSync(
        join(maliciousExtPath, 'gemini-extension.json'),
        otherExtension,
      );

      const symlinkPath = join(rig.testDir!, 'symlink-extension');
      symlinkSync(realExtPath, symlinkPath);

      // Function to run a command with a PTY to avoid headless mode
      const runPty = (args: string[]) => {
        const ptyProcess = pty.spawn(process.execPath, [BUNDLE_PATH, ...args], {
          name: 'xterm-color',
          cols: 80,
          rows: 80,
          cwd: rig.testDir!,
          env: {
            ...process.env,
            GEMINI_CLI_HOME: rig.homeDir!,
            GEMINI_CLI_INTEGRATION_TEST: 'true',
            GEMINI_PTY_INFO: 'node-pty',
          },
        });
        return new InteractiveRun(ptyProcess);
      };

      // 1. Install via symlink, trust it
      const run1 = runPty(['extensions', 'install', symlinkPath]);
      await run1.expectText('Do you want to trust this folder', 30000);
      await run1.type('y\r');
      await run1.expectText('trust this workspace', 30000);
      await run1.type('y\r');
      await run1.expectText('Do you want to continue', 30000);
      await run1.type('y\r');
      await run1.expectText('installed successfully', 30000);
      await run1.kill();

      // 2. Verify trustedFolders.json contains the REAL path, not the symlink path
      const trustedFoldersPath = join(
        rig.homeDir!,
        GEMINI_DIR,
        'trustedFolders.json',
      );
      // Wait for file to be written
      let attempts = 0;
      while (!fs.existsSync(trustedFoldersPath) && attempts < 50) {
        await new Promise((resolve) => setTimeout(resolve, 100));
        attempts++;
      }

      const trustedFolders = JSON.parse(
        readFileSync(trustedFoldersPath, 'utf-8'),
      );
      const trustedPaths = Object.keys(trustedFolders);
      const canonicalRealExtPath = fs.realpathSync(realExtPath);

      expect(trustedPaths).toContain(canonicalRealExtPath);
      expect(trustedPaths).not.toContain(symlinkPath);

      // 3. Swap the symlink to point to the malicious extension
      unlinkSync(symlinkPath);
      symlinkSync(maliciousExtPath, symlinkPath);

      // 4. Try to install again via the same symlink path.
      // It should NOT be trusted because the real path changed.
      const run2 = runPty(['extensions', 'install', symlinkPath]);
      await run2.expectText('Do you want to trust this folder', 30000);
      await run2.type('n\r');
      await run2.expectText('Installation aborted', 30000);
      await run2.kill();
    }, 60000);
  },
);
