// ABOUTME: Tests for schema-based tool validation system
// ABOUTME: Ensures tools validate inputs and handle errors correctly

import { describe, it, expect, beforeEach } from 'vitest';
import { z } from 'zod';
import { Tool } from './tool';
import { ToolContext, ToolResult } from './types';
import { join } from 'path';

// Test implementation of new Tool class
class TestTool extends Tool {
  name = 'test_tool';
  description = 'Test tool for validation';
  schema = z.object({
    required: z.string().min(1),
    optional: z.number().optional(),
  });

  protected executeValidated(
    args: z.infer<typeof this.schema>,
    _context: ToolContext
  ): Promise<ToolResult> {
    return Promise.resolve({
      content: [{ type: 'text' as const, text: `Got: ${args.required}` }],
      status: 'completed',
    });
  }
}

describe('Tool with schema validation', () => {
  it('validates and executes with valid parameters', async () => {
    const tool = new TestTool();
    const result = await tool.execute(
      { required: 'hello' },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('completed');
    expect(result.content[0].text).toBe('Got: hello');
  });

  it('handles optional parameters correctly', async () => {
    const tool = new TestTool();
    const result = await tool.execute(
      { required: 'hello', optional: 42 },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('completed');
    expect(result.content[0].text).toBe('Got: hello');
  });

  it('returns concise validation errors for invalid parameters', async () => {
    const tool = new TestTool();
    const result = await tool.execute({ optional: 123 }, { signal: new AbortController().signal }); // missing required field

    expect(result.status).toBe('failed');
    expect(result.content[0].text).toBe(
      'ValidationError: test_tool failed\nMissing required: required'
    );
  });

  it('validates parameter types correctly', async () => {
    const tool = new TestTool();
    const result = await tool.execute(
      { required: 'hello', optional: 'not-a-number' },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('failed');
    expect(result.content[0].text).toContain('ValidationError: test_tool failed');
    expect(result.content[0].text).toContain('optional: Expected number, got string');
  });

  it('rejects empty strings for required string fields', async () => {
    const tool = new TestTool();
    const result = await tool.execute({ required: '' }, { signal: new AbortController().signal });

    expect(result.status).toBe('failed');
    expect(result.content[0].text).toContain('ValidationError: test_tool failed');
    expect(result.content[0].text).toContain('required: String must contain at least 1 character');
  });

  it('generates JSON schema from Zod schema', () => {
    const tool = new TestTool();
    const jsonSchema = tool.inputSchema;

    expect(jsonSchema.type).toBe('object');
    expect(jsonSchema.properties.required).toBeDefined();
    expect(jsonSchema.properties.optional).toBeDefined();
    expect(jsonSchema.required).toContain('required');
    expect(jsonSchema.required).not.toContain('optional');
  });

  it('provides concise error messages with type mismatches', async () => {
    const tool = new TestTool();
    const result = await tool.execute({ required: null }, { signal: new AbortController().signal });

    expect(result.status).toBe('failed');
    expect(result.content[0].text).toContain('ValidationError: test_tool failed');
    expect(result.content[0].text).toContain('required: Expected string, got null');
  });

  it('handles unexpected parameters', async () => {
    const tool = new TestTool();
    const result = await tool.execute(
      { required: 'hello', extra: 'param', another: 123 },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('completed'); // Zod strips unknown keys by default
    expect(result.content[0].text).toBe('Got: hello');
  });

  it('handles multiple validation errors concisely', async () => {
    const tool = new TestTool();
    const result = await tool.execute(
      { optional: 'not-a-number', extraField: 'unexpected' },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('failed');
    const errorText = result.content[0].text;
    expect(errorText).toContain('ValidationError: test_tool failed');
    expect(errorText).toContain('Missing required: required');
    // Zod strips unknown keys, so extraField won't show as unexpected
    expect(errorText).toContain('optional: Expected number, got string');
  });
});

// Test complex validation scenarios
class ComplexTestTool extends Tool {
  name = 'complex_test';
  description = 'Tool with complex validation rules';
  schema = z
    .object({
      startLine: z.number().int().positive(),
      endLine: z.number().int().positive(),
      content: z.string().min(1),
    })
    .refine(
      (data) => {
        return data.endLine >= data.startLine;
      },
      {
        message: 'endLine must be >= startLine',
        path: ['endLine'],
      }
    );

  protected executeValidated(
    _args: z.infer<typeof this.schema>,
    _context: ToolContext
  ): Promise<ToolResult> {
    return Promise.resolve({
      content: [{ type: 'text' as const, text: 'validation passed' }],
      status: 'completed',
    });
  }
}

describe('Tool with complex validation', () => {
  it('validates cross-field constraints', async () => {
    const tool = new ComplexTestTool();
    const result = await tool.execute(
      {
        startLine: 5,
        endLine: 3, // Invalid: end before start
        content: 'test',
      },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('failed');
    expect(result.content[0].text).toContain('endLine must be >= startLine');
  });

  it('passes when cross-field constraints are met', async () => {
    const tool = new ComplexTestTool();
    const result = await tool.execute(
      {
        startLine: 3,
        endLine: 5,
        content: 'test',
      },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('completed');
  });

  it('reports the correct path for cross-field validation errors', async () => {
    const tool = new ComplexTestTool();
    const result = await tool.execute(
      {
        startLine: 10,
        endLine: 5,
        content: 'test',
      },
      { signal: new AbortController().signal }
    );

    expect(result.status).toBe('failed');
    expect(result.content[0].text).toContain('endLine: endLine must be >= startLine');
  });
});

// Test tool for temp directory functionality
class TempDirectoryTestTool extends Tool {
  name = 'temp_dir_test_tool';
  description = 'Tool for testing temp directory functionality';
  schema = z.object({
    message: z.string(),
  });

  protected async executeValidated(
    args: z.infer<typeof this.schema>,
    _context: ToolContext
  ): Promise<ToolResult> {
    return Promise.resolve(this.createResult(`Test executed: ${args.message}`));
  }

  // Expose protected methods for testing
  public getToolTempDirPublic(context?: ToolContext): string {
    return this.getToolTempDir(context);
  }

  public getOutputFilePathsPublic(context?: ToolContext) {
    // Use toolTempDir to create output file paths
    const toolTempDir = this.getToolTempDir(context);
    return {
      stdout: join(toolTempDir, 'stdout.txt'),
      stderr: join(toolTempDir, 'stderr.txt'),
      combined: join(toolTempDir, 'combined.txt'),
    };
  }
}

describe('Tool temp directory functionality', () => {
  let testTool: TempDirectoryTestTool;

  beforeEach(() => {
    testTool = new TempDirectoryTestTool();
  });

  it('should get output file paths from temp directory', () => {
    const context: ToolContext = {
      signal: new AbortController().signal,
      toolTempDir: '/tmp/test/tool-temp-dir',
    };

    const paths = testTool.getOutputFilePathsPublic(context);
    expect(paths.stdout).toBe('/tmp/test/tool-temp-dir/stdout.txt');
    expect(paths.stderr).toBe('/tmp/test/tool-temp-dir/stderr.txt');
    expect(paths.combined).toBe('/tmp/test/tool-temp-dir/combined.txt');
  });

  it('should throw error when temp directory not provided', () => {
    const context: ToolContext = {
      signal: new AbortController().signal,
      // No toolTempDir
    };

    expect(() => {
      testTool.getOutputFilePathsPublic(context);
    }).toThrow('Tool temp directory not provided by ToolExecutor');
  });

  it('should get tool temp dir from context', () => {
    const context: ToolContext = {
      signal: new AbortController().signal,
      toolTempDir: '/tmp/test/tool-call-123',
    };

    const tempDir = testTool.getToolTempDirPublic(context);
    expect(tempDir).toBe('/tmp/test/tool-call-123');
  });

  it('should throw error when tool temp dir not provided', () => {
    const context: ToolContext = {
      signal: new AbortController().signal,
      // No toolTempDir
    };

    expect(() => {
      testTool.getToolTempDirPublic(context);
    }).toThrow('Tool temp directory not provided by ToolExecutor');
  });
});
