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

import { describe, it, expect } from 'vitest';
import { createPolicyEngineConfig } from './policy.js';
import type { Settings } from './settings.js';
import {
  ApprovalMode,
  PolicyDecision,
  WEB_FETCH_TOOL_NAME,
} from '@google/gemini-cli-core';

describe('createPolicyEngineConfig', () => {
  it('should return ASK_USER for write tools and ALLOW for read-only tools by default', () => {
    const settings: Settings = {};
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
    expect(config.defaultDecision).toBe(PolicyDecision.ASK_USER);
    // The order of the rules is not guaranteed, so we sort them by tool name.
    config.rules?.sort((a, b) =>
      (a.toolName ?? '').localeCompare(b.toolName ?? ''),
    );
    expect(config.rules).toEqual([
      {
        toolName: 'glob',
        decision: PolicyDecision.ALLOW,
        priority: 50,
      },
      {
        toolName: 'google_web_search',
        decision: PolicyDecision.ALLOW,
        priority: 50,
      },
      {
        toolName: 'list_directory',
        decision: PolicyDecision.ALLOW,
        priority: 50,
      },
      {
        toolName: 'read_file',
        decision: PolicyDecision.ALLOW,
        priority: 50,
      },
      {
        toolName: 'read_many_files',
        decision: PolicyDecision.ALLOW,
        priority: 50,
      },
      {
        toolName: 'replace',
        decision: PolicyDecision.ASK_USER,
        priority: 10,
      },
      {
        toolName: 'run_shell_command',
        decision: PolicyDecision.ASK_USER,
        priority: 10,
      },
      {
        toolName: 'save_memory',
        decision: PolicyDecision.ASK_USER,
        priority: 10,
      },
      {
        toolName: 'search_file_content',
        decision: PolicyDecision.ALLOW,
        priority: 50,
      },
      {
        toolName: 'web_fetch',
        decision: PolicyDecision.ASK_USER,
        priority: 10,
      },
      {
        toolName: 'write_file',
        decision: PolicyDecision.ASK_USER,
        priority: 10,
      },
    ]);
  });

  it('should allow tools in tools.allowed', () => {
    const settings: Settings = {
      tools: { allowed: ['run_shell_command'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
    const rule = config.rules?.find(
      (r) =>
        r.toolName === 'run_shell_command' &&
        r.decision === PolicyDecision.ALLOW,
    );
    expect(rule).toBeDefined();
    expect(rule?.priority).toBe(100);
  });

  it('should deny tools in tools.exclude', () => {
    const settings: Settings = {
      tools: { exclude: ['run_shell_command'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
    const rule = config.rules?.find(
      (r) =>
        r.toolName === 'run_shell_command' &&
        r.decision === PolicyDecision.DENY,
    );
    expect(rule).toBeDefined();
    expect(rule?.priority).toBe(200);
  });

  it('should allow tools from allowed MCP servers', () => {
    const settings: Settings = {
      mcp: { allowed: ['my-server'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
    const rule = config.rules?.find(
      (r) =>
        r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW,
    );
    expect(rule).toBeDefined();
    expect(rule?.priority).toBe(85);
  });

  it('should deny tools from excluded MCP servers', () => {
    const settings: Settings = {
      mcp: { excluded: ['my-server'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
    const rule = config.rules?.find(
      (r) =>
        r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY,
    );
    expect(rule).toBeDefined();
    expect(rule?.priority).toBe(195);
  });

  it('should allow tools from trusted MCP servers', () => {
    const settings: Settings = {
      mcpServers: {
        'trusted-server': {
          command: 'node',
          args: ['server.js'],
          trust: true,
        },
        'untrusted-server': {
          command: 'node',
          args: ['server.js'],
          trust: false,
        },
      },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);

    const trustedRule = config.rules?.find(
      (r) =>
        r.toolName === 'trusted-server__*' &&
        r.decision === PolicyDecision.ALLOW,
    );
    expect(trustedRule).toBeDefined();
    expect(trustedRule?.priority).toBe(90);

    // Untrusted server should not have an allow rule
    const untrustedRule = config.rules?.find(
      (r) =>
        r.toolName === 'untrusted-server__*' &&
        r.decision === PolicyDecision.ALLOW,
    );
    expect(untrustedRule).toBeUndefined();
  });

  it('should handle multiple MCP server configurations together', () => {
    const settings: Settings = {
      mcp: {
        allowed: ['allowed-server'],
        excluded: ['excluded-server'],
      },
      mcpServers: {
        'trusted-server': {
          command: 'node',
          args: ['server.js'],
          trust: true,
        },
      },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);

    // Check allowed server
    const allowedRule = config.rules?.find(
      (r) =>
        r.toolName === 'allowed-server__*' &&
        r.decision === PolicyDecision.ALLOW,
    );
    expect(allowedRule).toBeDefined();
    expect(allowedRule?.priority).toBe(85);

    // Check trusted server
    const trustedRule = config.rules?.find(
      (r) =>
        r.toolName === 'trusted-server__*' &&
        r.decision === PolicyDecision.ALLOW,
    );
    expect(trustedRule).toBeDefined();
    expect(trustedRule?.priority).toBe(90);

    // Check excluded server
    const excludedRule = config.rules?.find(
      (r) =>
        r.toolName === 'excluded-server__*' &&
        r.decision === PolicyDecision.DENY,
    );
    expect(excludedRule).toBeDefined();
    expect(excludedRule?.priority).toBe(195);
  });

  it('should allow all tools in YOLO mode', () => {
    const settings: Settings = {};
    const config = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
    const rule = config.rules?.find(
      (r) => r.decision === PolicyDecision.ALLOW && r.priority === 0,
    );
    expect(rule).toBeDefined();
  });

  it('should allow edit tool in AUTO_EDIT mode', () => {
    const settings: Settings = {};
    const config = createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
    const rule = config.rules?.find(
      (r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW,
    );
    expect(rule).toBeDefined();
    expect(rule?.priority).toBe(15);
  });

  it('should prioritize exclude over allow', () => {
    const settings: Settings = {
      tools: { allowed: ['run_shell_command'], exclude: ['run_shell_command'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
    const denyRule = config.rules?.find(
      (r) =>
        r.toolName === 'run_shell_command' &&
        r.decision === PolicyDecision.DENY,
    );
    const allowRule = config.rules?.find(
      (r) =>
        r.toolName === 'run_shell_command' &&
        r.decision === PolicyDecision.ALLOW,
    );
    expect(denyRule).toBeDefined();
    expect(allowRule).toBeDefined();
    expect(denyRule!.priority).toBeGreaterThan(allowRule!.priority!);
  });

  it('should prioritize specific tool allows over MCP server excludes', () => {
    const settings: Settings = {
      mcp: { excluded: ['my-server'] },
      tools: { allowed: ['my-server__specific-tool'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);

    const serverDenyRule = config.rules?.find(
      (r) =>
        r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY,
    );
    const toolAllowRule = config.rules?.find(
      (r) =>
        r.toolName === 'my-server__specific-tool' &&
        r.decision === PolicyDecision.ALLOW,
    );

    expect(serverDenyRule).toBeDefined();
    expect(serverDenyRule?.priority).toBe(195);
    expect(toolAllowRule).toBeDefined();
    expect(toolAllowRule?.priority).toBe(100);

    // Tool allow (100) has lower priority than server deny (195),
    // so server deny wins - this might be counterintuitive
  });

  it('should prioritize specific tool excludes over MCP server allows', () => {
    const settings: Settings = {
      mcp: { allowed: ['my-server'] },
      mcpServers: {
        'my-server': {
          command: 'node',
          args: ['server.js'],
          trust: true,
        },
      },
      tools: { exclude: ['my-server__dangerous-tool'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);

    const serverAllowRule = config.rules?.find(
      (r) =>
        r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW,
    );
    const toolDenyRule = config.rules?.find(
      (r) =>
        r.toolName === 'my-server__dangerous-tool' &&
        r.decision === PolicyDecision.DENY,
    );

    expect(serverAllowRule).toBeDefined();
    expect(toolDenyRule).toBeDefined();
    expect(toolDenyRule!.priority).toBeGreaterThan(serverAllowRule!.priority!);
  });

  it('should handle complex priority scenarios correctly', () => {
    const settings: Settings = {
      tools: {
        autoAccept: true, // Priority 50 for read-only tools
        allowed: ['my-server__tool1', 'other-tool'], // Priority 100
        exclude: ['my-server__tool2', 'glob'], // Priority 200
      },
      mcp: {
        allowed: ['allowed-server'], // Priority 85
        excluded: ['excluded-server'], // Priority 195
      },
      mcpServers: {
        'trusted-server': {
          command: 'node',
          args: ['server.js'],
          trust: true, // Priority 90
        },
      },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);

    // Verify glob is denied even though autoAccept would allow it
    const globDenyRule = config.rules?.find(
      (r) => r.toolName === 'glob' && r.decision === PolicyDecision.DENY,
    );
    const globAllowRule = config.rules?.find(
      (r) => r.toolName === 'glob' && r.decision === PolicyDecision.ALLOW,
    );
    expect(globDenyRule).toBeDefined();
    expect(globAllowRule).toBeDefined();
    expect(globDenyRule!.priority).toBe(200);
    expect(globAllowRule!.priority).toBe(50);

    // Verify all priority levels are correct
    const priorities = config.rules
      ?.map((r) => ({
        tool: r.toolName,
        decision: r.decision,
        priority: r.priority,
      }))
      .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));

    // Check that the highest priority items are the excludes
    const highestPriorityExcludes = priorities?.filter(
      (p) => p.priority === 200,
    );
    expect(
      highestPriorityExcludes?.every((p) => p.decision === PolicyDecision.DENY),
    ).toBe(true);
  });

  it('should handle MCP servers with undefined trust property', () => {
    const settings: Settings = {
      mcpServers: {
        'no-trust-property': {
          command: 'node',
          args: ['server.js'],
          // trust property is undefined/missing
        },
        'explicit-false': {
          command: 'node',
          args: ['server.js'],
          trust: false,
        },
      },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);

    // Neither server should have an allow rule
    const noTrustRule = config.rules?.find(
      (r) =>
        r.toolName === 'no-trust-property__*' &&
        r.decision === PolicyDecision.ALLOW,
    );
    const explicitFalseRule = config.rules?.find(
      (r) =>
        r.toolName === 'explicit-false__*' &&
        r.decision === PolicyDecision.ALLOW,
    );

    expect(noTrustRule).toBeUndefined();
    expect(explicitFalseRule).toBeUndefined();
  });

  it('should not add write tool rules in YOLO mode', () => {
    const settings: Settings = {
      tools: { exclude: ['dangerous-tool'] },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.YOLO);

    // Should have the wildcard allow rule with priority 0
    const wildcardRule = config.rules?.find(
      (r) =>
        !r.toolName && r.decision === PolicyDecision.ALLOW && r.priority === 0,
    );
    expect(wildcardRule).toBeDefined();

    // Should NOT have any write tool rules (which would have priority 10)
    const writeToolRules = config.rules?.filter(
      (r) =>
        [
          'replace',
          'save_memory',
          'run_shell_command',
          'write_file',
          WEB_FETCH_TOOL_NAME,
        ].includes(r.toolName || '') && r.decision === PolicyDecision.ASK_USER,
    );
    expect(writeToolRules).toHaveLength(0);

    // Should still have the exclude rule
    const excludeRule = config.rules?.find(
      (r) =>
        r.toolName === 'dangerous-tool' && r.decision === PolicyDecision.DENY,
    );
    expect(excludeRule).toBeDefined();
    expect(excludeRule?.priority).toBe(200);
  });

  it('should handle combination of trusted server and excluded server for same name', () => {
    const settings: Settings = {
      mcpServers: {
        'conflicted-server': {
          command: 'node',
          args: ['server.js'],
          trust: true, // Priority 90
        },
      },
      mcp: {
        excluded: ['conflicted-server'], // Priority 195
      },
    };
    const config = createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);

    // Both rules should exist
    const trustRule = config.rules?.find(
      (r) =>
        r.toolName === 'conflicted-server__*' &&
        r.decision === PolicyDecision.ALLOW,
    );
    const excludeRule = config.rules?.find(
      (r) =>
        r.toolName === 'conflicted-server__*' &&
        r.decision === PolicyDecision.DENY,
    );

    expect(trustRule).toBeDefined();
    expect(trustRule?.priority).toBe(90);
    expect(excludeRule).toBeDefined();
    expect(excludeRule?.priority).toBe(195);

    // Exclude (195) should win over trust (90) when evaluated
  });

  it('should handle all approval modes correctly', () => {
    const settings: Settings = {};

    // Test DEFAULT mode
    const defaultConfig = createPolicyEngineConfig(
      settings,
      ApprovalMode.DEFAULT,
    );
    expect(defaultConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
    expect(
      defaultConfig.rules?.find(
        (r) => !r.toolName && r.decision === PolicyDecision.ALLOW,
      ),
    ).toBeUndefined();

    // Test YOLO mode
    const yoloConfig = createPolicyEngineConfig(settings, ApprovalMode.YOLO);
    expect(yoloConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
    const yoloWildcard = yoloConfig.rules?.find(
      (r) => !r.toolName && r.decision === PolicyDecision.ALLOW,
    );
    expect(yoloWildcard).toBeDefined();
    expect(yoloWildcard?.priority).toBe(0);

    // Test AUTO_EDIT mode
    const autoEditConfig = createPolicyEngineConfig(
      settings,
      ApprovalMode.AUTO_EDIT,
    );
    expect(autoEditConfig.defaultDecision).toBe(PolicyDecision.ASK_USER);
    const editRule = autoEditConfig.rules?.find(
      (r) => r.toolName === 'replace' && r.decision === PolicyDecision.ALLOW,
    );
    expect(editRule).toBeDefined();
    expect(editRule?.priority).toBe(15);
  });
});
