import { describe, expect, it } from 'vitest';
import { LIGHTS, PROPS,
  MAP_H, MAP_W, TILE, VENUE,
  isWalkable, isWalkableWorld, tileAt, tileColour, tileToWorld, worldToTile,
} from '../../src/scenes/floor/venueMap';
import type { AnchorKind, TileKind, WorldPoint } from '../../src/scenes/floor/venueMap';

const ANCHOR_KINDS: readonly AnchorKind[] = ['bar', 'dance', 'booth', 'toilet', 'smoke', 'entry', 'exit'];
const allAnchors = (): WorldPoint[] => ANCHOR_KINDS.flatMap((k) => [...VENUE.anchors[k]]);

const key = (tx: number, ty: number): number => ty * MAP_W + tx;

/** Every walkable tile 4-connected to the seed. */
function flood(seed: WorldPoint): Set<number> {
  const start = worldToTile(seed.x, seed.y);
  const seen = new Set<number>();
  if (!isWalkable(VENUE, start.tx, start.ty)) return seen;
  const stack = [start];
  seen.add(key(start.tx, start.ty));
  while (stack.length > 0) {
    const cur = stack.pop()!;
    for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const) {
      const tx = cur.tx + dx;
      const ty = cur.ty + dy;
      const k = key(tx, ty);
      if (seen.has(k) || !isWalkable(VENUE, tx, ty)) continue;
      seen.add(k);
      stack.push({ tx, ty });
    }
  }
  return seen;
}

describe('venueMap dimensions', () => {
  it('matches the shared geometry constants', () => {
    expect(TILE).toBe(16);
    expect(VENUE.width).toBe(MAP_W);
    expect(VENUE.height).toBe(MAP_H);
    expect(MAP_W * TILE).toBe(1280);
    expect(MAP_H * TILE).toBe(720);
  });

  it('has one tile per cell, all of known kinds', () => {
    expect(VENUE.tiles).toHaveLength(MAP_W * MAP_H);
    const known = new Set<TileKind>([
      'void', 'floor', 'wall', 'bar', 'stool', 'dance',
      'booth', 'stall', 'stallDoor', 'exit', 'smokeDoor', 'neon',
      'djbooth', 'djfloor', 'yard', 'fence', 'sink',
    ]);
    for (const t of VENUE.tiles) expect(known.has(t)).toBe(true);
  });

  it('is ringed by non-walkable edges apart from doors', () => {
    for (let tx = 0; tx < MAP_W; tx++) {
      for (const ty of [0, MAP_H - 1]) {
        const kind = tileAt(VENUE, tx, ty);
        if (kind === 'smokeDoor') continue;
        expect(isWalkable(VENUE, tx, ty)).toBe(false);
      }
    }
  });

  it('contains every authored tile kind', () => {
    const seen = new Set(VENUE.tiles);
    for (const k of ['floor', 'wall', 'bar', 'stool', 'dance', 'booth', 'stall', 'stallDoor', 'exit', 'smokeDoor', 'neon', 'djbooth', 'djfloor', 'yard', 'fence', 'sink'] as const) {
      expect(seen.has(k)).toBe(true);
    }
  });
});

describe('tileAt / walkability', () => {
  it('reports void out of bounds', () => {
    expect(tileAt(VENUE, -1, 0)).toBe('void');
    expect(tileAt(VENUE, 0, -1)).toBe('void');
    expect(tileAt(VENUE, MAP_W, 0)).toBe('void');
    expect(tileAt(VENUE, 0, MAP_H)).toBe('void');
    expect(isWalkable(VENUE, -5, -5)).toBe(false);
  });

  it('walks floor/dance/exit/smokeDoor/stall/yard and nothing else', () => {
    const walkable = new Set<TileKind>(['floor', 'dance', 'exit', 'smokeDoor', 'stall', 'yard']);
    for (let ty = 0; ty < MAP_H; ty++) {
      for (let tx = 0; tx < MAP_W; tx++) {
        expect(isWalkable(VENUE, tx, ty)).toBe(walkable.has(tileAt(VENUE, tx, ty)));
      }
    }
  });

  it('agrees between tile and world walkability queries', () => {
    for (const a of allAnchors()) expect(isWalkableWorld(VENUE, a.x, a.y)).toBe(true);
    expect(isWalkableWorld(VENUE, -8, -8)).toBe(false);
  });
});

describe('coordinate conversion', () => {
  it('round-trips tile -> world -> tile', () => {
    for (let ty = 0; ty < MAP_H; ty += 3) {
      for (let tx = 0; tx < MAP_W; tx += 3) {
        const w = tileToWorld(tx, ty);
        expect(worldToTile(w.x, w.y)).toEqual({ tx, ty });
      }
    }
  });

  it('returns tile centres, and floors world coords', () => {
    expect(tileToWorld(0, 0)).toEqual({ x: 8, y: 8 });
    expect(tileToWorld(3, 5)).toEqual({ x: 56, y: 88 });
    expect(worldToTile(0, 0)).toEqual({ tx: 0, ty: 0 });
    expect(worldToTile(15.9, 31.9)).toEqual({ tx: 0, ty: 1 });
  });
});

describe('anchors', () => {
  it('meets the minimum counts per kind', () => {
    const min: Record<AnchorKind, number> = {
      bar: 6, dance: 8, booth: 4, toilet: 4, smoke: 1, entry: 1, exit: 1,
    };
    for (const k of ANCHOR_KINDS) {
      expect(VENUE.anchors[k].length).toBeGreaterThanOrEqual(min[k]);
    }
  });

  it('places every anchor on a walkable tile centre', () => {
    for (const k of ANCHOR_KINDS) {
      for (const a of VENUE.anchors[k]) {
        expect(a.x % TILE).toBe(TILE / 2);
        expect(a.y % TILE).toBe(TILE / 2);
        expect(isWalkableWorld(VENUE, a.x, a.y)).toBe(true);
      }
    }
  });

  it('CRITICAL: every anchor is 4-connected to every other anchor', () => {
    const anchors = allAnchors();
    const first = anchors[0]!;
    const reachable = flood(first);
    for (const a of anchors) {
      const t = worldToTile(a.x, a.y);
      expect(
        reachable.has(key(t.tx, t.ty)),
        `anchor at tile ${t.tx},${t.ty} is cut off from ${JSON.stringify(worldToTile(first.x, first.y))}`,
      ).toBe(true);
    }
  });

  it('leaves no orphaned open floor — every non-stall walkable tile is reachable', () => {
    const reachable = flood(VENUE.anchors.entry[0]!);
    const orphans: string[] = [];
    for (let ty = 0; ty < MAP_H; ty++) {
      for (let tx = 0; tx < MAP_W; tx++) {
        if (!isWalkable(VENUE, tx, ty) || reachable.has(key(tx, ty))) continue;
        // Cubicle interiors are sealed behind their doors by design.
        if (tileAt(VENUE, tx, ty) === 'stall') continue;
        orphans.push(`${tx},${ty}`);
      }
    }
    expect(orphans, `unreachable open tiles: ${orphans.join(' ')}`).toEqual([]);
  });

  it('anchors of different kinds sit on distinct tiles within their kind', () => {
    for (const k of ANCHOR_KINDS) {
      const keys = VENUE.anchors[k].map((a) => {
        const t = worldToTile(a.x, a.y);
        return key(t.tx, t.ty);
      });
      expect(new Set(keys).size).toBe(keys.length);
    }
  });
});

describe('stalls', () => {
  it('has four cubicles with unique ids', () => {
    expect(VENUE.stalls).toHaveLength(4);
    expect(new Set(VENUE.stalls.map((s) => s.id)).size).toBe(4);
    for (const s of VENUE.stalls) expect(s.id).toMatch(/^s[1-4]$/);
  });

  it('door tiles are walkable and sit directly south of a stallDoor tile', () => {
    for (const s of VENUE.stalls) {
      const d = worldToTile(s.door.x, s.door.y);
      expect(isWalkable(VENUE, d.tx, d.ty)).toBe(true);
      expect(tileAt(VENUE, d.tx, d.ty - 1)).toBe('stallDoor');
    }
  });

  it('inside points land on stall tiles', () => {
    for (const s of VENUE.stalls) {
      // The 2x2 interior centre is a tile corner; probe just inside it.
      expect(isWalkableWorld(VENUE, s.inside.x - 1, s.inside.y - 1)).toBe(true);
      const t = worldToTile(s.inside.x - 1, s.inside.y - 1);
      expect(tileAt(VENUE, t.tx, t.ty)).toBe('stall');
    }
  });

  it('seals each cubicle behind its door — the only way in is StallDef.inside', () => {
    for (const s of VENUE.stalls) {
      const d = worldToTile(s.door.x, s.door.y);
      const reachable = flood(s.door);
      const interior = worldToTile(s.inside.x - 1, s.inside.y - 1);
      expect(reachable.has(key(interior.tx, interior.ty))).toBe(false);
      // ...and the door tile still connects back to the rest of the venue.
      expect(reachable.has(key(worldToTile(VENUE.anchors.entry[0]!.x, VENUE.anchors.entry[0]!.y).tx,
        worldToTile(VENUE.anchors.entry[0]!.x, VENUE.anchors.entry[0]!.y).ty))).toBe(true);
      expect(isWalkable(VENUE, d.tx, d.ty)).toBe(true);
    }
  });

  it('exposes one toilet anchor per stall door', () => {
    const doors = VENUE.stalls.map((s) => `${s.door.x},${s.door.y}`).sort();
    const anchors = VENUE.anchors.toilet.map((a) => `${a.x},${a.y}`).sort();
    expect(anchors).toEqual(doors);
  });
});

describe('tileColour', () => {
  it('gives every kind a distinct 24-bit colour', () => {
    const kinds: readonly TileKind[] = [
      'void', 'floor', 'wall', 'bar', 'stool', 'dance',
      'booth', 'stall', 'stallDoor', 'exit', 'smokeDoor', 'neon',
    ];
    const colours = kinds.map(tileColour);
    for (const c of colours) {
      expect(Number.isInteger(c)).toBe(true);
      expect(c).toBeGreaterThanOrEqual(0);
      expect(c).toBeLessThanOrEqual(0xffffff);
    }
    expect(new Set(colours).size).toBe(kinds.length);
  });
});

describe('props & lights (docs/SPACES.md)', () => {
  it('every prop sits in bounds with a positive footprint', () => {
    for (const p of PROPS) {
      expect(p.tw, p.id).toBeGreaterThan(0);
      expect(p.th, p.id).toBeGreaterThan(0);
      expect(p.tx, p.id).toBeGreaterThanOrEqual(0);
      expect(p.ty, p.id).toBeGreaterThanOrEqual(0);
      expect(p.tx + p.tw, p.id).toBeLessThanOrEqual(MAP_W);
      expect(p.ty + p.th, p.id).toBeLessThanOrEqual(MAP_H);
    }
  });

  it('prop ids are unique', () => {
    expect(new Set(PROPS.map((p) => p.id)).size).toBe(PROPS.length);
  });

  it('every zone light sits in bounds with a sane radius', () => {
    for (const l of LIGHTS) {
      expect(l.tx, l.id).toBeGreaterThanOrEqual(0);
      expect(l.tx, l.id).toBeLessThan(MAP_W);
      expect(l.ty, l.id).toBeGreaterThanOrEqual(0);
      expect(l.ty, l.id).toBeLessThan(MAP_H);
      expect(l.radius, l.id).toBeGreaterThan(4);
      expect(l.radius, l.id).toBeLessThan(120);
    }
  });

  it('the DJ booth interior is sealed — a stage, not a room', () => {
    for (let ty = 20; ty <= 24; ty++) {
      for (let tx = 53; tx <= 57; tx++) expect(isWalkable(VENUE, tx, ty)).toBe(false);
    }
  });

  it('the smoking yard is walkable and entered through its door', () => {
    expect(tileAt(VENUE, 18, 38)).toBe('smokeDoor');
    expect(isWalkable(VENUE, 18, 40)).toBe(true);
  });
});
