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

/// <reference types="vitest/globals" />

import { describe, it, expect } from 'vitest';
import {
  validateSettings,
  formatValidationError,
  settingsZodSchema,
} from './settings-validation.js';
import { z } from 'zod';
import { type Settings } from './settingsSchema.js';

describe('settings-validation', () => {
  describe('validateSettings', () => {
    it('should accept valid settings with correct model.name as string', () => {
      const validSettings = {
        model: {
          name: 'gemini-2.0-flash-exp',
          maxSessionTurns: 10,
        },
        ui: {
          theme: 'dark',
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should reject model.name as object instead of string', () => {
      const invalidSettings = {
        model: {
          name: {
            skipNextSpeakerCheck: true,
          },
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);
      expect(result.error).toBeDefined();

      if (result.error) {
        const issues = result.error.issues;
        expect(issues.length).toBeGreaterThan(0);
        expect(issues[0]?.path).toEqual(['model', 'name']);
        expect(issues[0]?.code).toBe('invalid_type');
      }
    });

    it('should accept valid model.summarizeToolOutput structure', () => {
      const validSettings = {
        model: {
          summarizeToolOutput: {
            run_shell_command: {
              tokenBudget: 500,
            },
          },
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should reject invalid model.summarizeToolOutput structure', () => {
      const invalidSettings = {
        model: {
          summarizeToolOutput: {
            run_shell_command: {
              tokenBudget: 500,
            },
          },
        },
      };

      // First test with valid structure
      let result = validateSettings(invalidSettings);
      expect(result.success).toBe(true);

      // Now test with wrong type (string instead of object)
      const actuallyInvalidSettings = {
        model: {
          summarizeToolOutput: 'invalid',
        },
      };

      result = validateSettings(actuallyInvalidSettings);
      expect(result.success).toBe(false);
      if (result.error) {
        expect(result.error.issues.length).toBeGreaterThan(0);
      }
    });

    it('should accept empty settings object', () => {
      const emptySettings = {};
      const result = validateSettings(emptySettings);
      expect(result.success).toBe(true);
    });

    it('should accept unknown top-level keys (for migration compatibility)', () => {
      const settingsWithUnknownKey = {
        unknownKey: 'some value',
      };

      const result = validateSettings(settingsWithUnknownKey);
      expect(result.success).toBe(true);
      // Unknown keys are allowed via .passthrough() for migration scenarios
    });

    it('should accept nested valid settings', () => {
      const validSettings = {
        ui: {
          theme: 'dark',
          hideWindowTitle: true,
          footer: {
            hideCWD: false,
            hideModelInfo: true,
          },
        },
        tools: {
          sandbox: 'inherit',
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should validate array types correctly', () => {
      const validSettings = {
        tools: {
          allowed: ['git', 'npm'],
          exclude: ['dangerous-tool'],
        },
        context: {
          includeDirectories: ['/path/1', '/path/2'],
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should reject invalid types in arrays', () => {
      const invalidSettings = {
        tools: {
          allowed: ['git', 123],
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);
    });

    it('should validate boolean fields correctly', () => {
      const validSettings = {
        general: {
          vimMode: true,
          disableAutoUpdate: false,
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should reject non-boolean values for boolean fields', () => {
      const invalidSettings = {
        general: {
          vimMode: 'yes',
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);
    });

    it('should validate number fields correctly', () => {
      const validSettings = {
        model: {
          maxSessionTurns: 50,
          compressionThreshold: 0.2,
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should validate complex nested mcpServers configuration', () => {
      const invalidSettings = {
        mcpServers: {
          'my-server': {
            command: 123, // Should be string
            args: ['arg1'],
            env: {
              VAR: 'value',
            },
          },
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);
      if (result.error) {
        expect(result.error.issues.length).toBeGreaterThan(0);
        // Path should be mcpServers.my-server.command
        const issue = result.error.issues.find((i) =>
          i.path.includes('command'),
        );
        expect(issue).toBeDefined();
        expect(issue?.code).toBe('invalid_type');
      }
    });

    it('should validate mcpServers with type field for all transport types', () => {
      const validSettings = {
        mcpServers: {
          'sse-server': {
            url: 'https://example.com/sse',
            type: 'sse',
            headers: { 'X-API-Key': 'key' },
          },
          'http-server': {
            url: 'https://example.com/mcp',
            type: 'http',
          },
          'stdio-server': {
            command: '/usr/bin/mcp-server',
            type: 'stdio',
          },
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should reject invalid type values in mcpServers', () => {
      const invalidSettings = {
        mcpServers: {
          'bad-server': {
            url: 'https://example.com/mcp',
            type: 'invalid-type',
          },
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);
    });

    it('should validate mcpServers without type field', () => {
      const validSettings = {
        mcpServers: {
          'stdio-server': {
            command: '/usr/bin/mcp-server',
            args: ['--port', '8080'],
          },
          'url-server': {
            url: 'https://example.com/mcp',
          },
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    it('should validate complex nested customThemes configuration', () => {
      const invalidSettings = {
        ui: {
          customThemes: {
            'my-theme': {
              type: 'custom',
              // Missing 'name' property which is required
              text: {
                primary: '#ffffff',
              },
            },
          },
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);
      if (result.error) {
        expect(result.error.issues.length).toBeGreaterThan(0);
        // Should complain about missing 'name'
        const issue = result.error.issues.find(
          (i) => i.code === 'invalid_type' && i.message.includes('Required'),
        );
        expect(issue).toBeDefined();
      }
    });

    it('should accept customThemes with text.response color override', () => {
      // Regression test for #25610: `response` is a documented and
      // implemented color override for model responses (see
      // packages/cli/src/ui/themes/theme.ts and semantic-tokens.ts),
      // but was missing from the CustomTheme validation schema.
      const validSettings = {
        ui: {
          theme: 'LimeWhite',
          customThemes: {
            LimeWhite: {
              type: 'custom',
              name: 'LimeWhite',
              text: {
                primary: '#00FF00',
                response: '#FFFFFF',
                secondary: '#a0a0a0',
                accent: '#00FF00',
              },
            },
          },
        },
      };

      const result = validateSettings(validSettings);
      expect(result.success).toBe(true);
    });

    describe('type casting', () => {
      it('should cast "true" and "false" strings to booleans', () => {
        const settings = {
          ui: {
            autoThemeSwitching: 'true',
            hideWindowTitle: 'false',
          },
        };

        const result = validateSettings(settings);
        expect(result.success).toBe(true);
        const data = result.data as Settings;
        expect(data.ui?.autoThemeSwitching).toBe(true);
        expect(data.ui?.hideWindowTitle).toBe(false);
      });

      it('should cast boolean strings case-insensitively', () => {
        const settings = {
          ui: {
            autoThemeSwitching: 'TRUE',
            hideWindowTitle: 'fAlSe',
          },
        };

        const result = validateSettings(settings);
        expect(result.success).toBe(true);
        const data = result.data as Settings;
        expect(data.ui?.autoThemeSwitching).toBe(true);
        expect(data.ui?.hideWindowTitle).toBe(false);
      });

      it('should cast numeric strings to numbers', () => {
        const settings = {
          model: {
            maxSessionTurns: '42',
            compressionThreshold: '0.5',
          },
        };

        const result = validateSettings(settings);
        expect(result.success).toBe(true);
        const data = result.data as Settings;
        expect(data.model?.maxSessionTurns).toBe(42);
        expect(data.model?.compressionThreshold).toBe(0.5);
      });

      it('should reject invalid castable strings', () => {
        const settings = {
          ui: {
            autoThemeSwitching: 'not-a-boolean',
          },
          model: {
            maxSessionTurns: 'not-a-number',
          },
        };

        const result = validateSettings(settings);
        expect(result.success).toBe(false);
        expect(result.error?.issues).toHaveLength(2);
        expect(result.error?.issues[0].message).toContain(
          'Expected boolean, received string',
        );
        expect(result.error?.issues[1].message).toContain(
          'Expected number, received string',
        );
      });

      it('should cast strings to booleans/numbers in shared definitions (refs)', () => {
        const settings = {
          mcpServers: {
            'test-server': {
              command: 'node',
              trust: 'true', // from boolean ref
            },
          },
        };

        const result = validateSettings(settings);
        expect(result.success).toBe(true);
        const data = result.data as Settings;
        expect(data.mcpServers?.['test-server'].trust).toBe(true);
      });
    });
  });

  describe('formatValidationError', () => {
    it('should format error with file path and helpful message for model.name', () => {
      const invalidSettings = {
        model: {
          name: {
            skipNextSpeakerCheck: true,
          },
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);

      if (result.error) {
        const formatted = formatValidationError(
          result.error,
          '/path/to/settings.json',
        );

        expect(formatted).toContain('/path/to/settings.json');
        expect(formatted).toContain('model.name');
        expect(formatted).toContain('Expected: string, but received: object');
        expect(formatted).toContain('Please fix the configuration.');
        expect(formatted).toContain(
          'https://geminicli.com/docs/reference/configuration/',
        );
      }
    });

    it('should format error for model.summarizeToolOutput', () => {
      const invalidSettings = {
        model: {
          summarizeToolOutput: 'wrong type',
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);

      if (result.error) {
        const formatted = formatValidationError(
          result.error,
          '~/.gemini/settings.json',
        );

        expect(formatted).toContain('~/.gemini/settings.json');
        expect(formatted).toContain('model.summarizeToolOutput');
      }
    });

    it('should include link to documentation', () => {
      const invalidSettings = {
        model: {
          name: { invalid: 'object' }, // model.name should be a string
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);

      if (result.error) {
        const formatted = formatValidationError(result.error, 'test.json');

        expect(formatted).toContain(
          'https://geminicli.com/docs/reference/configuration/',
        );
      }
    });

    it('should list all validation errors', () => {
      const invalidSettings = {
        model: {
          name: { invalid: 'object' },
          maxSessionTurns: 'not a number',
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);

      if (result.error) {
        const formatted = formatValidationError(result.error, 'test.json');

        // Should have multiple errors listed
        expect(formatted.match(/Error in:/g)?.length).toBeGreaterThan(1);
      }
    });

    it('should format array paths correctly (e.g. tools.allowed[0])', () => {
      const invalidSettings = {
        tools: {
          allowed: ['git', 123], // 123 is invalid, expected string
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);

      if (result.error) {
        const formatted = formatValidationError(result.error, 'test.json');
        expect(formatted).toContain('tools.allowed[1]');
      }
    });

    it('should limit the number of displayed errors', () => {
      const invalidSettings = {
        tools: {
          // Create 6 invalid items to trigger the limit
          allowed: [1, 2, 3, 4, 5, 6],
        },
      };

      const result = validateSettings(invalidSettings);
      expect(result.success).toBe(false);

      if (result.error) {
        const formatted = formatValidationError(result.error, 'test.json');
        // Should see the first 5
        expect(formatted).toContain('tools.allowed[0]');
        expect(formatted).toContain('tools.allowed[4]');
        // Should NOT see the 6th
        expect(formatted).not.toContain('tools.allowed[5]');
        // Should see the summary
        expect(formatted).toContain('...and 1 more errors.');
      }
    });
  });

  describe('settingsZodSchema', () => {
    it('should be a valid Zod object schema', () => {
      expect(settingsZodSchema).toBeInstanceOf(z.ZodObject);
    });

    it('should have optional fields', () => {
      // All top-level fields should be optional
      const shape = settingsZodSchema.shape;
      expect(shape['model']).toBeDefined();
      expect(shape['ui']).toBeDefined();
      expect(shape['tools']).toBeDefined();

      // Test that empty object is valid (all fields optional)
      const result = settingsZodSchema.safeParse({});
      expect(result.success).toBe(true);
    });
  });
});
