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

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { debugLogger, TOOL_OUTPUTS_DIR } from '@google/gemini-cli-core';
import type { Settings } from '../config/settings.js';
import { cleanupToolOutputFiles } from './sessionCleanup.js';

describe('Tool Output Cleanup', () => {
  let testTempDir: string;

  beforeEach(async () => {
    // Create a unique temp directory for each test
    testTempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tool-output-test-'));
    vi.spyOn(debugLogger, 'error').mockImplementation(() => {});
    vi.spyOn(debugLogger, 'warn').mockImplementation(() => {});
    vi.spyOn(debugLogger, 'debug').mockImplementation(() => {});
  });

  afterEach(async () => {
    vi.restoreAllMocks();
    // Clean up the temp directory
    try {
      await fs.rm(testTempDir, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors
    }
  });

  describe('cleanupToolOutputFiles', () => {
    it('should return early when cleanup is disabled', async () => {
      const settings: Settings = {
        general: { sessionRetention: { enabled: false } },
      };

      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.disabled).toBe(true);
      expect(result.scanned).toBe(0);
      expect(result.deleted).toBe(0);
      expect(result.failed).toBe(0);
    });

    it('should return early when sessionRetention is not configured', async () => {
      const settings: Settings = {};

      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.disabled).toBe(true);
      expect(result.scanned).toBe(0);
      expect(result.deleted).toBe(0);
    });

    it('should return early when tool-outputs directory does not exist', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxAge: '7d',
          },
        },
      };

      // Don't create the tool-outputs directory
      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.disabled).toBe(false);
      expect(result.scanned).toBe(0);
      expect(result.deleted).toBe(0);
      expect(result.failed).toBe(0);
    });

    it('should delete files older than maxAge', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxAge: '7d',
          },
        },
      };

      // Create tool-outputs directory and files
      const toolOutputDir = path.join(testTempDir, TOOL_OUTPUTS_DIR);
      await fs.mkdir(toolOutputDir, { recursive: true });

      const now = Date.now();
      const fiveDaysAgo = now - 5 * 24 * 60 * 60 * 1000;
      const tenDaysAgo = now - 10 * 24 * 60 * 60 * 1000;

      // Create files with different ages
      const recentFile = path.join(toolOutputDir, 'shell_recent.txt');
      const oldFile = path.join(toolOutputDir, 'shell_old.txt');

      await fs.writeFile(recentFile, 'recent content');
      await fs.writeFile(oldFile, 'old content');

      // Set file modification times
      await fs.utimes(recentFile, fiveDaysAgo / 1000, fiveDaysAgo / 1000);
      await fs.utimes(oldFile, tenDaysAgo / 1000, tenDaysAgo / 1000);

      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.disabled).toBe(false);
      expect(result.scanned).toBe(2);
      expect(result.deleted).toBe(1); // Only the 10-day-old file should be deleted
      expect(result.failed).toBe(0);

      // Verify the old file was deleted and recent file remains
      const remainingFiles = await fs.readdir(toolOutputDir);
      expect(remainingFiles).toContain('shell_recent.txt');
      expect(remainingFiles).not.toContain('shell_old.txt');
    });

    it('should delete oldest files when exceeding maxCount', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxCount: 2,
          },
        },
      };

      // Create tool-outputs directory and files
      const toolOutputDir = path.join(testTempDir, TOOL_OUTPUTS_DIR);
      await fs.mkdir(toolOutputDir, { recursive: true });

      const now = Date.now();
      const oneDayAgo = now - 1 * 24 * 60 * 60 * 1000;
      const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000;
      const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1000;

      // Create 3 files with different ages
      const file1 = path.join(toolOutputDir, 'shell_1.txt');
      const file2 = path.join(toolOutputDir, 'shell_2.txt');
      const file3 = path.join(toolOutputDir, 'shell_3.txt');

      await fs.writeFile(file1, 'content 1');
      await fs.writeFile(file2, 'content 2');
      await fs.writeFile(file3, 'content 3');

      // Set file modification times (file3 is oldest)
      await fs.utimes(file1, oneDayAgo / 1000, oneDayAgo / 1000);
      await fs.utimes(file2, twoDaysAgo / 1000, twoDaysAgo / 1000);
      await fs.utimes(file3, threeDaysAgo / 1000, threeDaysAgo / 1000);

      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.disabled).toBe(false);
      expect(result.scanned).toBe(3);
      expect(result.deleted).toBe(1); // Should delete 1 file to get down to maxCount of 2
      expect(result.failed).toBe(0);

      // Verify the oldest file was deleted
      const remainingFiles = await fs.readdir(toolOutputDir);
      expect(remainingFiles).toHaveLength(2);
      expect(remainingFiles).not.toContain('shell_3.txt');
    });

    it('should handle empty directory', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxAge: '7d',
          },
        },
      };

      // Create empty tool-outputs directory
      const toolOutputDir = path.join(testTempDir, TOOL_OUTPUTS_DIR);
      await fs.mkdir(toolOutputDir, { recursive: true });

      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.disabled).toBe(false);
      expect(result.scanned).toBe(0);
      expect(result.deleted).toBe(0);
      expect(result.failed).toBe(0);
    });

    it('should apply both maxAge and maxCount together', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxAge: '3d',
            maxCount: 2,
          },
        },
      };

      // Create tool-outputs directory and files
      const toolOutputDir = path.join(testTempDir, TOOL_OUTPUTS_DIR);
      await fs.mkdir(toolOutputDir, { recursive: true });

      const now = Date.now();
      const oneDayAgo = now - 1 * 24 * 60 * 60 * 1000;
      const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000;
      const twoAndHalfDaysAgo = now - 2.5 * 24 * 60 * 60 * 1000;
      const fiveDaysAgo = now - 5 * 24 * 60 * 60 * 1000;
      const tenDaysAgo = now - 10 * 24 * 60 * 60 * 1000;

      // Create 5 files with different ages
      const file1 = path.join(toolOutputDir, 'shell_1.txt'); // 1 day old - keep
      const file2 = path.join(toolOutputDir, 'shell_2.txt'); // 2 days old - keep
      const file3 = path.join(toolOutputDir, 'shell_3.txt'); // 2.5 days old - delete by count
      const file4 = path.join(toolOutputDir, 'shell_4.txt'); // 5 days old - delete by age
      const file5 = path.join(toolOutputDir, 'shell_5.txt'); // 10 days old - delete by age

      await fs.writeFile(file1, 'content 1');
      await fs.writeFile(file2, 'content 2');
      await fs.writeFile(file3, 'content 3');
      await fs.writeFile(file4, 'content 4');
      await fs.writeFile(file5, 'content 5');

      // Set file modification times
      await fs.utimes(file1, oneDayAgo / 1000, oneDayAgo / 1000);
      await fs.utimes(file2, twoDaysAgo / 1000, twoDaysAgo / 1000);
      await fs.utimes(
        file3,
        twoAndHalfDaysAgo / 1000,
        twoAndHalfDaysAgo / 1000,
      );
      await fs.utimes(file4, fiveDaysAgo / 1000, fiveDaysAgo / 1000);
      await fs.utimes(file5, tenDaysAgo / 1000, tenDaysAgo / 1000);

      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.disabled).toBe(false);
      expect(result.scanned).toBe(5);
      // file4 and file5 deleted by maxAge, file3 deleted by maxCount
      expect(result.deleted).toBe(3);
      expect(result.failed).toBe(0);

      // Verify only the 2 newest files remain
      const remainingFiles = await fs.readdir(toolOutputDir);
      expect(remainingFiles).toHaveLength(2);
      expect(remainingFiles).toContain('shell_1.txt');
      expect(remainingFiles).toContain('shell_2.txt');
      expect(remainingFiles).not.toContain('shell_3.txt');
      expect(remainingFiles).not.toContain('shell_4.txt');
      expect(remainingFiles).not.toContain('shell_5.txt');
    });

    it('should log debug information when enabled', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxAge: '1d',
          },
        },
      };

      // Create tool-outputs directory and an old file
      const toolOutputDir = path.join(testTempDir, TOOL_OUTPUTS_DIR);
      await fs.mkdir(toolOutputDir, { recursive: true });

      const tenDaysAgo = Date.now() - 10 * 24 * 60 * 60 * 1000;
      const oldFile = path.join(toolOutputDir, 'shell_old.txt');
      await fs.writeFile(oldFile, 'old content');
      await fs.utimes(oldFile, tenDaysAgo / 1000, tenDaysAgo / 1000);

      const debugSpy = vi
        .spyOn(debugLogger, 'debug')
        .mockImplementation(() => {});

      await cleanupToolOutputFiles(settings, true, testTempDir);

      expect(debugSpy).toHaveBeenCalledWith(
        expect.stringContaining('Tool output cleanup: deleted'),
      );

      debugSpy.mockRestore();
    });

    it('should delete expired session subdirectories', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxAge: '1d',
          },
        },
      };

      const toolOutputDir = path.join(testTempDir, TOOL_OUTPUTS_DIR);
      await fs.mkdir(toolOutputDir, { recursive: true });

      const now = Date.now();
      const tenDaysAgo = now - 10 * 24 * 60 * 60 * 1000;
      const oneHourAgo = now - 1 * 60 * 60 * 1000;

      const oldSessionDir = path.join(toolOutputDir, 'session-old');
      const recentSessionDir = path.join(toolOutputDir, 'session-recent');

      await fs.mkdir(oldSessionDir);
      await fs.mkdir(recentSessionDir);

      // Set modification times
      await fs.utimes(oldSessionDir, tenDaysAgo / 1000, tenDaysAgo / 1000);
      await fs.utimes(recentSessionDir, oneHourAgo / 1000, oneHourAgo / 1000);

      const result = await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(result.deleted).toBe(1);
      const remainingDirs = await fs.readdir(toolOutputDir);
      expect(remainingDirs).toContain('session-recent');
      expect(remainingDirs).not.toContain('session-old');
    });

    it('should skip subdirectories with path traversal characters', async () => {
      const settings: Settings = {
        general: {
          sessionRetention: {
            enabled: true,
            maxAge: '1d',
          },
        },
      };

      const toolOutputDir = path.join(testTempDir, TOOL_OUTPUTS_DIR);
      await fs.mkdir(toolOutputDir, { recursive: true });

      // Create an unsafe directory name
      const unsafeDir = path.join(toolOutputDir, 'session-.._.._danger');
      await fs.mkdir(unsafeDir, { recursive: true });

      const debugSpy = vi
        .spyOn(debugLogger, 'debug')
        .mockImplementation(() => {});

      await cleanupToolOutputFiles(settings, false, testTempDir);

      expect(debugSpy).toHaveBeenCalledWith(
        expect.stringContaining('Skipping unsafe tool-output subdirectory'),
      );

      // Directory should still exist (it was skipped, not deleted)
      const entries = await fs.readdir(toolOutputDir);
      expect(entries).toContain('session-.._.._danger');

      debugSpy.mockRestore();
    });
  });
});
