import { describe, it, expect } from 'vitest';
import {
  distance,
  snapToGrid,
  snapPointToGrid,
  clamp,
  projectPointOntoSegment,
  segmentAngle,
  pointInPolygon,
  boundingBox,
  rectsOverlap,
  generateLocalId,
} from '../geometry';

describe('distance', () => {
  it('returns 0 for same point', () => {
    expect(distance({ x: 1, y: 2 }, { x: 1, y: 2 })).toBe(0);
  });

  it('computes horizontal distance', () => {
    expect(distance({ x: 0, y: 0 }, { x: 3, y: 0 })).toBe(3);
  });

  it('computes vertical distance', () => {
    expect(distance({ x: 0, y: 0 }, { x: 0, y: 4 })).toBe(4);
  });

  it('computes diagonal distance (3-4-5 triangle)', () => {
    expect(distance({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5);
  });

  it('works with negative coordinates', () => {
    expect(distance({ x: -1, y: -1 }, { x: 2, y: 3 })).toBe(5);
  });
});

describe('snapToGrid', () => {
  it('snaps to nearest multiple', () => {
    expect(snapToGrid(0.14, 0.1)).toBeCloseTo(0.1);
  });

  it('snaps exactly on grid', () => {
    expect(snapToGrid(0.2, 0.1)).toBeCloseTo(0.2);
  });

  it('rounds up when closer to next grid', () => {
    expect(snapToGrid(0.16, 0.1)).toBeCloseTo(0.2);
  });

  it('snaps to 0 for values near 0', () => {
    expect(snapToGrid(0.04, 0.1)).toBeCloseTo(0);
  });
});

describe('snapPointToGrid', () => {
  it('snaps both x and y', () => {
    const result = snapPointToGrid({ x: 0.14, y: 0.27 }, 0.1);
    expect(result.x).toBeCloseTo(0.1);
    expect(result.y).toBeCloseTo(0.3);
  });
});

describe('clamp', () => {
  it('returns value when in range', () => {
    expect(clamp(5, 0, 10)).toBe(5);
  });

  it('clamps to min', () => {
    expect(clamp(-1, 0, 10)).toBe(0);
  });

  it('clamps to max', () => {
    expect(clamp(15, 0, 10)).toBe(10);
  });

  it('handles min === max', () => {
    expect(clamp(5, 3, 3)).toBe(3);
  });
});

describe('projectPointOntoSegment', () => {
  it('projects onto midpoint of horizontal segment', () => {
    const result = projectPointOntoSegment(
      { x: 5, y: 3 },
      { x: 0, y: 0 },
      { x: 10, y: 0 },
    );
    expect(result.projected.x).toBeCloseTo(5);
    expect(result.projected.y).toBeCloseTo(0);
    expect(result.t).toBeCloseTo(0.5);
    expect(result.distance).toBeCloseTo(3);
  });

  it('clamps to start of segment', () => {
    const result = projectPointOntoSegment(
      { x: -5, y: 0 },
      { x: 0, y: 0 },
      { x: 10, y: 0 },
    );
    expect(result.projected.x).toBeCloseTo(0);
    expect(result.t).toBeCloseTo(0);
  });

  it('clamps to end of segment', () => {
    const result = projectPointOntoSegment(
      { x: 15, y: 0 },
      { x: 0, y: 0 },
      { x: 10, y: 0 },
    );
    expect(result.projected.x).toBeCloseTo(10);
    expect(result.t).toBeCloseTo(1);
  });

  it('handles zero-length segment', () => {
    const result = projectPointOntoSegment(
      { x: 3, y: 4 },
      { x: 0, y: 0 },
      { x: 0, y: 0 },
    );
    expect(result.t).toBe(0);
    expect(result.distance).toBe(5);
  });
});

describe('segmentAngle', () => {
  it('returns 0 for rightward segment', () => {
    expect(segmentAngle({ x: 0, y: 0 }, { x: 1, y: 0 })).toBeCloseTo(0);
  });

  it('returns PI/2 for downward segment', () => {
    expect(segmentAngle({ x: 0, y: 0 }, { x: 0, y: 1 })).toBeCloseTo(Math.PI / 2);
  });

  it('returns PI for leftward segment', () => {
    expect(segmentAngle({ x: 0, y: 0 }, { x: -1, y: 0 })).toBeCloseTo(Math.PI);
  });

  it('returns -PI/2 for upward segment', () => {
    expect(segmentAngle({ x: 0, y: 0 }, { x: 0, y: -1 })).toBeCloseTo(-Math.PI / 2);
  });
});

describe('pointInPolygon', () => {
  const square = [
    { x: 0, y: 0 },
    { x: 10, y: 0 },
    { x: 10, y: 10 },
    { x: 0, y: 10 },
  ];

  it('returns true for point inside', () => {
    expect(pointInPolygon({ x: 5, y: 5 }, square)).toBe(true);
  });

  it('returns false for point outside', () => {
    expect(pointInPolygon({ x: 15, y: 5 }, square)).toBe(false);
  });

  it('returns false for point far outside', () => {
    expect(pointInPolygon({ x: -5, y: -5 }, square)).toBe(false);
  });

  it('handles triangle', () => {
    const triangle = [
      { x: 0, y: 0 },
      { x: 10, y: 0 },
      { x: 5, y: 10 },
    ];
    expect(pointInPolygon({ x: 5, y: 3 }, triangle)).toBe(true);
    expect(pointInPolygon({ x: 9, y: 9 }, triangle)).toBe(false);
  });
});

describe('boundingBox', () => {
  it('returns zero-box for empty array', () => {
    const bb = boundingBox([]);
    expect(bb).toEqual({ minX: 0, minY: 0, maxX: 0, maxY: 0 });
  });

  it('returns point for single point', () => {
    const bb = boundingBox([{ x: 3, y: 5 }]);
    expect(bb).toEqual({ minX: 3, minY: 5, maxX: 3, maxY: 5 });
  });

  it('computes correct bounding box', () => {
    const bb = boundingBox([
      { x: -1, y: 2 },
      { x: 5, y: -3 },
      { x: 3, y: 7 },
    ]);
    expect(bb).toEqual({ minX: -1, minY: -3, maxX: 5, maxY: 7 });
  });
});

describe('rectsOverlap', () => {
  it('returns true for overlapping rects', () => {
    expect(
      rectsOverlap(
        { x: 0, y: 0, width: 5, height: 5 },
        { x: 3, y: 3, width: 5, height: 5 },
      ),
    ).toBe(true);
  });

  it('returns false for non-overlapping rects', () => {
    expect(
      rectsOverlap(
        { x: 0, y: 0, width: 5, height: 5 },
        { x: 10, y: 10, width: 5, height: 5 },
      ),
    ).toBe(false);
  });

  it('returns false for adjacent rects (touching edges)', () => {
    expect(
      rectsOverlap(
        { x: 0, y: 0, width: 5, height: 5 },
        { x: 5, y: 0, width: 5, height: 5 },
      ),
    ).toBe(false);
  });

  it('returns true for contained rect', () => {
    expect(
      rectsOverlap(
        { x: 0, y: 0, width: 10, height: 10 },
        { x: 2, y: 2, width: 3, height: 3 },
      ),
    ).toBe(true);
  });
});

describe('generateLocalId', () => {
  it('starts with "local-"', () => {
    expect(generateLocalId()).toMatch(/^local-/);
  });

  it('generates unique ids', () => {
    const ids = new Set(Array.from({ length: 100 }, () => generateLocalId()));
    expect(ids.size).toBe(100);
  });
});
