/**
 * Tests for groundingMetrics — pure functions, no I/O, fast.
 *
 * Why these matter: every accuracy claim in the bench (and in
 * production audit) depends on parseCoords / pointInRect / summarise
 * producing exactly the values everyone expects.
 */

import { describe, test, expect } from '@jest/globals';
import {
  parseCoords,
  distance,
  rectCenter,
  pointInRect,
  summarise,
} from '../groundingMetrics.js';

describe('parseCoords', () => {
  test('canonical form "x=N, y=N"', () => {
    expect(parseCoords('x=100, y=200')).toEqual({ x: 100, y: 200 });
  });

  test('forgiving of whitespace + punctuation', () => {
    expect(parseCoords('x  =   100 ;  y =  200'))   .toEqual({ x: 100, y: 200 });
    expect(parseCoords('   x=1, y=2.\n'))            .toEqual({ x: 1, y: 2 });
    expect(parseCoords('the answer is: x=42, y=99')).toEqual({ x: 42, y: 99 });
  });

  test('case insensitive', () => {
    expect(parseCoords('X=10, Y=20')).toEqual({ x: 10, y: 20 });
  });

  test('returns null on no match', () => {
    expect(parseCoords('')).toBeNull();
    expect(parseCoords('no coords here')).toBeNull();
    expect(parseCoords(undefined)).toBeNull();
    expect(parseCoords(null)).toBeNull();
    expect(parseCoords(42)).toBeNull();
  });

  test('rejects malformed numbers', () => {
    expect(parseCoords('x=abc, y=def')).toBeNull();
  });

  test('first match wins (model may emit examples)', () => {
    expect(parseCoords('the format is x=N, y=N. Answer: x=300, y=400'))
      .toEqual({ x: 300, y: 400 });
  });

  test('handles negative coords (edge case for cropped views)', () => {
    expect(parseCoords('x=-5, y=-10')).toEqual({ x: -5, y: -10 });
  });
});

describe('distance', () => {
  test('Euclidean for typical points', () => {
    expect(distance({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5);
  });

  test('zero when points coincide', () => {
    expect(distance({ x: 7, y: 7 }, { x: 7, y: 7 })).toBe(0);
  });

  test('NaN on invalid inputs (does not throw)', () => {
    expect(Number.isNaN(distance(null, { x: 1, y: 1 }))).toBe(true);
    expect(Number.isNaN(distance({ x: 1, y: 1 }, { x: 'a', y: 1 }))).toBe(true);
  });
});

describe('rectCenter', () => {
  test('returns geometric center', () => {
    expect(rectCenter({ x: 100, y: 100, width: 200, height: 50 }))
      .toEqual({ x: 200, y: 125 });
  });

  test('handles odd dimensions (returns fractional)', () => {
    expect(rectCenter({ x: 0, y: 0, width: 1, height: 1 }))
      .toEqual({ x: 0.5, y: 0.5 });
  });
});

describe('pointInRect', () => {
  const RECT = { x: 100, y: 100, width: 50, height: 30 };

  test('inside', () => {
    expect(pointInRect({ x: 120, y: 110 }, RECT)).toBe(true);
  });

  test('on boundary counts as inside', () => {
    expect(pointInRect({ x: 100, y: 100 }, RECT)).toBe(true);   // top-left
    expect(pointInRect({ x: 150, y: 130 }, RECT)).toBe(true);   // bot-right
  });

  test('outside', () => {
    expect(pointInRect({ x: 99,  y: 110 }, RECT)).toBe(false);
    expect(pointInRect({ x: 151, y: 110 }, RECT)).toBe(false);
    expect(pointInRect({ x: 120, y: 99  }, RECT)).toBe(false);
    expect(pointInRect({ x: 120, y: 131 }, RECT)).toBe(false);
  });

  test('false on invalid inputs', () => {
    expect(pointInRect(null, RECT)).toBe(false);
    expect(pointInRect({ x: 1, y: 1 }, null)).toBe(false);
    expect(pointInRect({ x: 1, y: 1 }, { x: 0, y: 0 })).toBe(false); // missing w/h
  });
});

describe('summarise', () => {
  test('aggregates a typical trial set', () => {
    const trials = [
      { err: 10, hit: true,  elapsedMs:  100, tokens: 500 },
      { err: 20, hit: true,  elapsedMs:  200, tokens: 800 },
      { err: 50, hit: false, elapsedMs:  300, tokens: 600 },
    ];
    const s = summarise(trials);
    expect(s.n).toBe(3);
    expect(s.hitRate).toBeCloseTo(2 / 3, 3);
    expect(s.meanErrPx).toBeCloseTo((10 + 20 + 50) / 3, 3);
    expect(s.meanLatencyMs).toBe(200);
    expect(s.meanTokens).toBeCloseTo((500 + 800 + 600) / 3, 3);
    expect(s.stdevErrPx).toBeGreaterThan(0);
  });

  test('returns zero summary on empty / invalid input', () => {
    const z = summarise([]);
    expect(z.n).toBe(0);
    expect(z.hitRate).toBe(0);
    expect(summarise(null).n).toBe(0);
  });

  test('handles trials with no err field (skip-on-no-coord scenario)', () => {
    const trials = [
      { hit: false, elapsedMs: 50, tokens: 100 },  // no err: model didn't produce coords
      { err: 5, hit: true, elapsedMs: 60, tokens: 200 },
    ];
    const s = summarise(trials);
    expect(s.meanErrPx).toBe(5);    // only the valid trial counts
    expect(s.hitRate).toBe(0.5);    // both trials counted for hit rate
  });

  test('stdev=0 with single trial (cannot compute without 2 samples)', () => {
    const s = summarise([{ err: 10, hit: true, elapsedMs: 100, tokens: 500 }]);
    expect(s.stdevErrPx).toBe(0);
  });

  test('p50 and p95 are sorted-percentile lookups', () => {
    const trials = Array.from({ length: 20 }, (_, i) => ({
      err: i, hit: true, elapsedMs: (i + 1) * 100, tokens: 100,
    }));
    const s = summarise(trials);
    // sorted ascending: 100..2000; p50 → ~1000ms, p95 → ~1900ms
    expect(s.p50LatencyMs).toBeGreaterThanOrEqual(900);
    expect(s.p50LatencyMs).toBeLessThanOrEqual(1100);
    expect(s.p95LatencyMs).toBeGreaterThanOrEqual(1800);
  });
});
