import { describe, expect, it } from 'vitest';
import {
  computeTotal,
  gradeChecklist,
  gradeInstructionsProgrammatic,
  gradeTools,
} from './grader.js';
import type { BenchTask, RawJobResult, ToolCallObservation } from './types.js';

function tc(name: string, summary: string): ToolCallObservation {
  const filePath = /(input|output)\/[\w\-./ ]+/.exec(summary)?.[0];
  return { name, inputSummary: summary, filePath };
}

function makeRaw(overrides: Partial<RawJobResult>): RawJobResult {
  return {
    taskId: 1,
    jobId: 'job-1',
    status: 'succeeded',
    iterations: null,
    promptTokens: null,
    completionTokens: null,
    workspacePath: '/tmp/x',
    activityLog: '',
    toolCalls: [],
    outputFiles: {},
    durationMs: 1000,
    ...overrides,
  };
}

const baseTask: BenchTask = {
  id: 'unit-test',
  title: 't',
  prompt: 'p',
  expected: { must_use_tools: [], forbidden_tools: [] },
};

describe('gradeTools', () => {
  it('rewards must_use_tools and penalizes forbidden tools', () => {
    const task: BenchTask = {
      ...baseTask,
      expected: { must_use_tools: ['ReadExcel', 'Write'], forbidden_tools: ['Bash'] },
    };
    const raw = makeRaw({ toolCalls: [tc('ReadExcel', 'input/x.xlsx'), tc('Write', 'output/x.md')] });
    const r = gradeTools(task, raw);
    expect(r.score).toBe(1);
  });

  it('penalizes forbidden tool use', () => {
    const task: BenchTask = {
      ...baseTask,
      expected: { must_use_tools: ['Write'], forbidden_tools: ['Bash'] },
    };
    const raw = makeRaw({ toolCalls: [tc('Write', 'output/x.md'), tc('Bash', 'rm -rf /tmp/x')] });
    const r = gradeTools(task, raw);
    expect(r.score).toBeLessThan(1);
  });

  it('detects forbidden_tool_for_ext (Read on .xlsx)', () => {
    const task: BenchTask = {
      ...baseTask,
      expected: {
        must_use_tools: [],
        forbidden_tool_for_ext: { Read: ['.xlsx'] },
      },
    };
    const raw = makeRaw({ toolCalls: [tc('Read', 'input/data.xlsx')] });
    const r = gradeTools(task, raw);
    expect(r.score).toBeLessThan(1);
    expect(r.details.some((d) => d.includes('Read on .xlsx'))).toBe(true);
  });

  it('does not penalize Read on .txt when xlsx is forbidden', () => {
    const task: BenchTask = {
      ...baseTask,
      expected: {
        must_use_tools: ['Read'],
        forbidden_tool_for_ext: { Read: ['.xlsx'] },
      },
    };
    const raw = makeRaw({ toolCalls: [tc('Read', 'input/notes.md')] });
    const r = gradeTools(task, raw);
    expect(r.score).toBe(1);
  });
});

describe('gradeChecklist', () => {
  const task: BenchTask = {
    ...baseTask,
    checklist: { required_tools: ['CreateChecklist', 'CheckItem', 'GetChecklist'], min_check_item_calls: 3 },
  };

  it('full credit when all 3 required tools used and CheckItem >= min', () => {
    const raw = makeRaw({
      toolCalls: [
        tc('CreateChecklist', '...'),
        tc('CheckItem', '...'),
        tc('CheckItem', '...'),
        tc('CheckItem', '...'),
        tc('GetChecklist', '...'),
      ],
    });
    expect(gradeChecklist(task, raw).score).toBe(1);
  });

  it('partial credit when CheckItem under min', () => {
    const raw = makeRaw({
      toolCalls: [tc('CreateChecklist', '...'), tc('CheckItem', '...'), tc('GetChecklist', '...')],
    });
    expect(gradeChecklist(task, raw).score).toBeCloseTo(2 / 3, 5);
  });

  it('zero when no checklist tool used', () => {
    expect(gradeChecklist(task, makeRaw({})).score).toBe(0);
  });

  it('returns 1 when checklist is not configured', () => {
    expect(gradeChecklist(baseTask, makeRaw({})).score).toBe(1);
  });
});

describe('gradeInstructionsProgrammatic', () => {
  it('penalizes when status is not in expected', () => {
    const task: BenchTask = {
      ...baseTask,
      expected: { completion_status: ['succeeded'] },
    };
    const raw = makeRaw({ status: 'failed' });
    const r = gradeInstructionsProgrammatic(task, raw);
    expect(r.score).toBe(0);
  });

  it('checks file existence and constraints', () => {
    const task: BenchTask = {
      ...baseTask,
      expected: { must_produce_files: ['output/report.md'] },
      grading: {
        programmatic: {
          constraints: [
            { type: 'file_first_line_equals', file: 'output/report.md', line: '# サマリー' },
            { type: 'file_must_contain_in_order', file: 'output/report.md', sections: ['## A', '## B'] },
            { type: 'file_section_max_lines', file: 'output/report.md', section: 'A', max: 2 },
            { type: 'file_line_starts_with', file: 'output/report.md', prefix: '-', min_lines: 2, section: 'B' },
            { type: 'file_line_max_chars', file: 'output/report.md', max: 30, section: 'B' },
            { type: 'file_no_pattern', file: 'output/report.md', pattern: '!\\[' },
          ],
        },
      },
    };
    const goodOutput = [
      '# サマリー',
      '',
      '## A',
      'short line',
      'short line 2',
      '',
      '## B',
      '- 短い行 1',
      '- 短い行 2',
    ].join('\n');
    const raw = makeRaw({ outputFiles: { 'output/report.md': goodOutput } });
    expect(gradeInstructionsProgrammatic(task, raw).score).toBe(1);
  });

  it('flags out-of-order sections', () => {
    const task: BenchTask = {
      ...baseTask,
      grading: {
        programmatic: {
          constraints: [
            { type: 'file_must_contain_in_order', file: 'out.md', sections: ['## A', '## B', '## C'] },
          ],
        },
      },
    };
    const wrongOrder = '# H\n## C\n## B\n## A\n';
    const raw = makeRaw({ outputFiles: { 'out.md': wrongOrder } });
    const r = gradeInstructionsProgrammatic(task, raw);
    expect(r.score).toBeLessThan(1);
  });

  it('flags forbidden Markdown image patterns', () => {
    const task: BenchTask = {
      ...baseTask,
      grading: {
        programmatic: {
          constraints: [{ type: 'file_no_pattern', file: 'out.md', pattern: '!\\[' }],
        },
      },
    };
    const withImg = '# H\n\n![alt](img.png)\n';
    const raw = makeRaw({ outputFiles: { 'out.md': withImg } });
    const r = gradeInstructionsProgrammatic(task, raw);
    // 1 status check (true) + 1 forbidden pattern check (false) = 1/2 = 0.5
    expect(r.score).toBeLessThan(1);
    expect(r.details.some((d) => d.includes('no_pattern'))).toBe(true);
  });
});

describe('computeTotal', () => {
  it('weights axes 30/15/30/25', () => {
    const total = computeTotal({
      tools: { score: 1, details: [] },
      checklist: { score: 1, details: [] },
      instructions: { score: 1, details: [] },
      reasoning: { score: 1, details: [] },
    });
    expect(total).toBe(100);
  });

  it('partial credit example', () => {
    const total = computeTotal({
      tools: { score: 0.9, details: [] },         // 27
      checklist: { score: 1.0, details: [] },     // 15
      instructions: { score: 0.7, details: [] },  // 21
      reasoning: { score: 0.7, details: [] },     // 17.5 → 17 or 18 after rounding
    });
    // 27 + 15 + 21 + 17.5 = 80.5 → 81
    expect(total).toBe(81);
  });
});
