import { describe, expect, it } from 'vitest';
import {
  NORMAL_CONE, UV_CONE, angleDelta, coneVisibility, conePolygon, inCone,
} from '../../src/scenes/floor/cone';
import type { ConeSpec } from '../../src/scenes/floor/cone';

const EAST: ConeSpec = { x: 100, y: 100, facing: 0, ...NORMAL_CONE };
const at = (spec: ConeSpec, dist: number, off: number) => ({
  x: spec.x + Math.cos(spec.facing + off) * dist,
  y: spec.y + Math.sin(spec.facing + off) * dist,
});

describe('angleDelta', () => {
  it('is zero for identical angles and full turns apart', () => {
    expect(angleDelta(0, 0)).toBeCloseTo(0, 12);
    expect(angleDelta(Math.PI * 2, 0)).toBeCloseTo(0, 12);
    expect(angleDelta(-Math.PI * 4, 0)).toBeCloseTo(0, 12);
  });

  it('takes the short way around the wrap point', () => {
    expect(angleDelta(0.1, -0.1)).toBeCloseTo(0.2, 12);
    // 175 deg and -175 deg are 10 deg apart, not 350.
    const a = Math.PI - 0.087;
    const b = -Math.PI + 0.087;
    expect(Math.abs(angleDelta(a, b))).toBeCloseTo(0.174, 6);
    expect(Math.abs(angleDelta(b, a))).toBeCloseTo(0.174, 6);
  });

  it('stays in (-PI, PI] for a sweep of inputs', () => {
    for (let i = -40; i <= 40; i++) {
      const d = angleDelta(i * 0.37, i * -0.61);
      expect(d).toBeGreaterThan(-Math.PI);
      expect(d).toBeLessThanOrEqual(Math.PI + 1e-12);
    }
  });

  it('maps an exact half turn to +PI, not -PI', () => {
    expect(angleDelta(Math.PI, 0)).toBeCloseTo(Math.PI, 12);
    expect(angleDelta(0, Math.PI)).toBeCloseTo(Math.PI, 12);
  });
});

describe('inCone', () => {
  it('sees straight ahead', () => {
    expect(inCone(EAST, 200, 100)).toBe(true);
    expect(inCone(EAST, EAST.x, EAST.y)).toBe(true);
  });

  it('is blind behind and to the sides', () => {
    expect(inCone(EAST, 0, 100)).toBe(false);
    expect(inCone(EAST, 100, 20)).toBe(false);
    expect(inCone(EAST, ...([at(EAST, 50, 1.2).x, at(EAST, 50, 1.2).y] as const))).toBe(false);
  });

  it('is blind beyond range', () => {
    const far = at(EAST, EAST.range + 1, 0);
    expect(inCone(EAST, far.x, far.y)).toBe(false);
    const near = at(EAST, EAST.range - 1, 0);
    expect(inCone(EAST, near.x, near.y)).toBe(true);
  });

  it('respects facing — rotating the cone rotates what it sees', () => {
    const north: ConeSpec = { ...EAST, facing: -Math.PI / 2 };
    expect(inCone(north, 100, 20)).toBe(true);
    expect(inCone(north, 200, 100)).toBe(false);
  });

  it('works across the PI wrap', () => {
    const west: ConeSpec = { ...EAST, facing: Math.PI };
    const p = at(west, 60, 0.2);
    expect(inCone(west, p.x, p.y)).toBe(true);
    expect(inCone(west, 200, 100)).toBe(false);
  });

  it('UV torch is tighter and shorter than the normal one', () => {
    expect(UV_CONE.halfAngle).toBeLessThan(NORMAL_CONE.halfAngle);
    expect(UV_CONE.range).toBeLessThan(NORMAL_CONE.range);
    const uv: ConeSpec = { x: 100, y: 100, facing: 0, ...UV_CONE };
    const p = at(EAST, 160, 0.5);
    expect(inCone(EAST, p.x, p.y)).toBe(true);
    expect(inCone(uv, p.x, p.y)).toBe(false);
  });
});

describe('coneVisibility', () => {
  it('is 0 wherever inCone is false', () => {
    for (let off = -2; off <= 2; off += 0.1) {
      for (const dist of [10, 90, 189, 191, 400]) {
        const p = at(EAST, dist, off);
        if (!inCone(EAST, p.x, p.y)) expect(coneVisibility(EAST, p.x, p.y)).toBe(0);
      }
    }
  });

  it('is strictly positive inside and never above 1', () => {
    for (const off of [0, 0.2, -0.2, 0.5, -0.5]) {
      for (const dist of [1, 40, 120, 180]) {
        const p = at(EAST, dist, off);
        const v = coneVisibility(EAST, p.x, p.y);
        expect(v).toBeGreaterThan(0);
        expect(v).toBeLessThanOrEqual(1);
      }
    }
    expect(coneVisibility(EAST, EAST.x, EAST.y)).toBe(1);
  });

  it('is brighter on-axis than off-axis at the same distance', () => {
    const onAxis = at(EAST, 100, 0);
    const offAxis = at(EAST, 100, 0.4);
    expect(coneVisibility(EAST, onAxis.x, onAxis.y))
      .toBeGreaterThan(coneVisibility(EAST, offAxis.x, offAxis.y));
  });

  it('decreases monotonically with distance along the axis', () => {
    let prev = Infinity;
    for (let d = 1; d < EAST.range; d += 10) {
      const p = at(EAST, d, 0);
      const v = coneVisibility(EAST, p.x, p.y);
      expect(v).toBeLessThan(prev);
      prev = v;
    }
  });

  it('fades to nothing at the cone edge', () => {
    const edge = at(EAST, 100, EAST.halfAngle);
    expect(coneVisibility(EAST, edge.x, edge.y)).toBeCloseTo(0, 9);
    const rim = at(EAST, EAST.range, 0);
    expect(coneVisibility(EAST, rim.x, rim.y)).toBeCloseTo(0, 9);
  });
});

describe('conePolygon', () => {
  it('starts at the apex and closes back on it', () => {
    const pts = conePolygon(EAST, 12);
    expect(pts[0]).toEqual({ x: EAST.x, y: EAST.y });
    expect(pts[pts.length - 1]).toEqual(pts[0]);
    expect(pts).toHaveLength(12 + 3); // apex + (segments+1) arc points + closing apex
  });

  it('puts every arc point at exactly range, inside the cone', () => {
    const pts = conePolygon(EAST, 16).slice(1, -1);
    for (const p of pts) {
      expect(Math.hypot(p.x - EAST.x, p.y - EAST.y)).toBeCloseTo(EAST.range, 9);
      expect(Math.abs(angleDelta(Math.atan2(p.y - EAST.y, p.x - EAST.x), EAST.facing)))
        .toBeLessThanOrEqual(EAST.halfAngle + 1e-9);
    }
  });

  it('spans the full cone width, edge to edge', () => {
    const pts = conePolygon(EAST, 8);
    const firstArc = pts[1]!;
    const lastArc = pts[pts.length - 2]!;
    expect(angleDelta(Math.atan2(firstArc.y - EAST.y, firstArc.x - EAST.x), EAST.facing))
      .toBeCloseTo(-EAST.halfAngle, 9);
    expect(angleDelta(Math.atan2(lastArc.y - EAST.y, lastArc.x - EAST.x), EAST.facing))
      .toBeCloseTo(EAST.halfAngle, 9);
  });

  it('tolerates a degenerate segment count', () => {
    expect(conePolygon(EAST, 0)).toHaveLength(4);
    expect(conePolygon(EAST, 1)).toHaveLength(4);
  });

  it('defaults to a smooth arc', () => {
    expect(conePolygon(EAST).length).toBeGreaterThan(20);
  });
});
