const {
  attackPositioning,
  rangeBand,
  sceneBattlemapTheme,
  validateBattlemapLayout,
  createBattlemapPositions,
  coverForPoint,
  moveWithinRange,
  defaultBattlemapLayout,
  wallBetween,
  visibilityBetween,
  DEFAULT_ZONES,
  DEFAULT_STATIONS,
  DEFAULT_SPAWN_POINTS,
  DEFAULT_COVER,
} = require('../shared/battlemapEngine');

describe('battlemapEngine', () => {
  test('applies short range, cover, and elevation modifiers', () => {
    const layout = {
      // 'low' touches 'high' at x=4. The shared edge only opens as a narrow
      // centered doorway, so this shot intentionally passes through that
      // doorway rather than through the whole wall seam.
      zones: [
        { id: 'high', x: 0, y: 0, w: 4, h: 4, elevation: 1 },
        { id: 'low', x: 4, y: 0, w: 8, h: 4, elevation: 0 },
      ],
      cover: [
        { id: 'c1', x: 9, y: 0, w: 2, h: 2, kind: 'light', material: 'Light Metal' },
      ],
      blockers: [],
    };
    const result = attackPositioning(layout, { x: 1, y: 2 }, { x: 10, y: 2 }, { name: 'Godwyn Bolter', range: '100m', pen: 0 }, 'ranged');
    expect(result.range.band).toBe('standard');
    expect(result.coverAp).toBe(4);
    expect(result.totalModifier).toBe(0);
    expect(result.reasons.join(' ')).toMatch(/Higher ground/);
  });

  test('marks melee attacks out of range', () => {
    const result = attackPositioning({ zones: [], cover: [], blockers: [] }, { x: 1, y: 1 }, { x: 6, y: 1 }, { name: 'Chainsword' }, 'melee');
    expect(result.range.band).toBe('melee_out_of_range');
    expect(result.totalModifier).toBe(-60);
  });

  test('blocks a ranged shot with an ally standing in the line of fire', () => {
    const layout = { zones: [], cover: [], blockers: [] };
    const attacker = { x: 1, y: 1 };
    const defender = { x: 10, y: 1 };
    const allyInTheWay = { x: 5, y: 1 };
    const result = attackPositioning(layout, attacker, defender, { name: 'Bolter', range: '100m' }, 'ranged', [allyInTheWay]);
    expect(result.blocked).toBe(true);
    expect(result.totalModifier).toBe(-60);
    expect(result.reasons.join(' ')).toMatch(/ally/i);
  });

  test('does not block a ranged shot when the ally is off the firing line', () => {
    const layout = { zones: [], cover: [], blockers: [] };
    const attacker = { x: 1, y: 1 };
    const defender = { x: 10, y: 1 };
    const allyOffLine = { x: 5, y: 6 };
    const result = attackPositioning(layout, attacker, defender, { name: 'Bolter', range: '100m' }, 'ranged', [allyOffLine]);
    expect(result.blocked).toBe(false);
  });

  test('does not block adjacent melee attacks even with an ally listed', () => {
    const layout = { zones: [], cover: [], blockers: [] };
    const attacker = { x: 1, y: 1 };
    const defender = { x: 2, y: 1 };
    const result = attackPositioning(layout, attacker, defender, { name: 'Chainsword' }, 'melee', [{ x: 1.5, y: 1 }]);
    expect(result.blocked).toBe(false);
    expect(result.range.band).toBe('melee');
  });

  test('does not treat the attacker or target themselves as blocking allies', () => {
    const layout = { zones: [], cover: [], blockers: [] };
    const attacker = { x: 1, y: 1 };
    const defender = { x: 10, y: 1 };
    const result = attackPositioning(layout, attacker, defender, { name: 'Bolter', range: '100m' }, 'ranged', [attacker, defender]);
    expect(result.blocked).toBe(false);
  });

  // Regression: attacks previously only checked the small decorative
  // `blockers` list (blast doors, containment cylinders) for line of sight,
  // never the room walls themselves — so combatants in different,
  // non-adjacent rooms could shoot straight through a solid wall.
  test('blocks a ranged shot between two rooms that do not share a doorway', () => {
    const layout = { zones: DEFAULT_ZONES, cover: [], blockers: [] };
    const leftWingPoint = { x: 15, y: 6 };
    const rightWingPoint = { x: 30, y: 6 };
    const result = attackPositioning(layout, leftWingPoint, rightWingPoint, { name: 'Bolter', range: '100m' }, 'ranged');
    expect(result.blocked).toBe(true);
    expect(result.reasons.join(' ')).toMatch(/wall/i);
  });

  test('blocks a melee swing through a wall even at close range', () => {
    const layout = { zones: DEFAULT_ZONES, cover: [], blockers: [] };
    // Just inside left-wing vs just inside the corridor's non-doorway top
    // wall region isn't adjacent here; use two points close together but on
    // opposite sides of left-wing's east wall (x=22).
    const insideLeftWing = { x: 21.8, y: 6 };
    const insideRightWing = { x: 25.2, y: 6 };
    const result = attackPositioning(layout, insideLeftWing, insideRightWing, { name: 'Chainsword' }, 'melee');
    expect(result.blocked).toBe(true);
  });

  test('does not block a shot within the same open room or through an actual doorway', () => {
    const layout = { zones: DEFAULT_ZONES, cover: [], blockers: [] };
    const corridorA = { x: 10, y: 15 };
    const corridorB = { x: 28, y: 15 };
    expect(wallBetween(layout, corridorA, corridorB)).toBeNull();

    const leftWingDoor = { x: 17, y: 12 };
    const corridorPoint = { x: 17, y: 15 };
    expect(wallBetween(layout, leftWingDoor, corridorPoint)).toBeNull();
  });

  test('blocks a shot that tries to cross a shared edge outside the doorway opening', () => {
    const layout = { zones: DEFAULT_ZONES, cover: [], blockers: [] };
    const leftWingWall = { x: 13.5, y: 12 };
    const corridorPoint = { x: 13.5, y: 15 };
    expect(wallBetween(layout, leftWingWall, corridorPoint)).not.toBeNull();
  });

  test('visibility is fully denied through walls even before attack logic runs', () => {
    const layout = { zones: DEFAULT_ZONES, cover: [], blockers: [] };
    const leftWingPoint = { x: 15, y: 6 };
    const rightWingPoint = { x: 30, y: 6 };
    const result = visibilityBetween(layout, leftWingPoint, rightWingPoint);
    expect(result.visible).toBe(false);
    expect(result.reasons.join(' ')).toMatch(/wall/i);
  });

  test('darkness reduces detection range even without a wall', () => {
    const layout = {
      zones: [
        { id: 'dark-lane', x: 0, y: 0, w: 30, h: 4, lighting: 'dark' },
      ],
      cover: [],
      blockers: [],
    };
    const result = visibilityBetween(layout, { x: 1, y: 2 }, { x: 18, y: 2 });
    expect(result.visible).toBe(false);
    expect(result.reasons.join(' ')).toMatch(/Visibility limit/i);
  });

  test('rejects corner-only room contact as a valid connection', () => {
    const zones = [
      { id: 'spawn', tone: 'spawn', x: 0, y: 0, w: 4, h: 4 },
      { id: 'corner-touch', tone: 'objective', x: 4, y: 4, w: 4, h: 4 },
    ];
    const issues = validateBattlemapLayout(zones);
    expect(issues.some(issue => issue.includes('corner-touch') && issue.includes('share an edge'))).toBe(true);
  });

  test('uses scene theme to fit landing scenes', () => {
    const theme = sceneBattlemapTheme({ title: 'Landing Goes Wrong' });
    expect(theme.boardTitle).toBe('Landing Zone Breach');
  });

  test('range band matches local modifier table', () => {
    expect(rangeBand(2, 100)).toEqual({ band: 'point_blank', modifier: 30 });
    expect(rangeBand(40, 100)).toEqual({ band: 'short', modifier: 10 });
    expect(rangeBand(150, 100)).toEqual({ band: 'long', modifier: -10 });
    expect(rangeBand(350, 100)).toEqual({ band: 'extreme', modifier: -30 });
  });
});

describe('validateBattlemapLayout', () => {
  test('the shipped default layout has no overlapping rooms', () => {
    // Spawn points (DEFAULT_SPAWN_POINTS) intentionally sit outside every room —
    // they're off-map entry hatches enemies/players arrive through, not anchors
    // that must resolve to a room. Only room-vs-room overlap is checked here.
    expect(validateBattlemapLayout(DEFAULT_ZONES)).toEqual([]);
  });

  test('flags two non-corridor rooms that overlap (regression: right-wing/vault crossed borders)', () => {
    const zones = [
      { id: 'right-wing', tone: 'objective', x: 25, y: 3, w: 10, h: 8 },
      { id: 'vault', tone: 'vault', x: 23, y: 9, w: 12, h: 12 },
    ];
    const issues = validateBattlemapLayout(zones);
    expect(issues.some(issue => issue.includes('right-wing') && issue.includes('vault'))).toBe(true);
  });

  // Rooms and the corridor are the wall base: they connect by sharing a
  // touching edge (zero overlap), never by overlapping — a corridor drawn
  // through a room's rectangle is just as much a crossed-border bug as two
  // rooms overlapping each other. No zone is exempt from this check anymore.
  test('flags a corridor overlapping a room (no exemptions)', () => {
    const zones = [
      { id: 'corridor', tone: 'corridor', x: 0, y: 0, w: 10, h: 4 },
      { id: 'room', tone: 'objective', x: 5, y: 0, w: 10, h: 10 },
    ];
    const issues = validateBattlemapLayout(zones);
    expect(issues.some(issue => issue.includes('corridor') && issue.includes('room'))).toBe(true);
  });

  test('accepts a corridor and a room that only share a touching edge (a doorway, not an overlap)', () => {
    const zones = [
      { id: 'corridor', tone: 'corridor', x: 0, y: 0, w: 10, h: 4 },
      { id: 'room', tone: 'objective', x: 5, y: 4, w: 10, h: 10 },
    ];
    expect(validateBattlemapLayout(zones)).toEqual([]);
  });

  test('flags a room that exists but is not reachable from the spawn zone', () => {
    const zones = [
      { id: 'extraction', tone: 'spawn', x: 0, y: 0, w: 4, h: 4 },
      { id: 'corridor', tone: 'corridor', x: 4, y: 0, w: 10, h: 4 },
      { id: 'isolated-room', tone: 'objective', x: 20, y: 20, w: 6, h: 6 },
    ];
    const issues = validateBattlemapLayout(zones);
    expect(issues.some(issue => issue.includes('isolated-room') && issue.includes('not reachable'))).toBe(true);
  });

  test('flags a point that falls outside every room (regression: enemy anchor outside all rooms)', () => {
    const zones = [
      { id: 'vault', tone: 'vault', x: 23, y: 9, w: 12, h: 12 },
      { id: 'maintenance', tone: 'hazard', x: 14, y: 22, w: 12, h: 6 },
    ];
    const issues = validateBattlemapLayout(zones, [[31.5, 24.5]]);
    expect(issues.some(issue => issue.includes('does not fall inside any zone'))).toBe(true);
  });

  test('flags a zone that extends past the map bounds', () => {
    const zones = [{ id: 'oob', tone: 'objective', x: 35, y: 0, w: 10, h: 5 }];
    const issues = validateBattlemapLayout(zones);
    expect(issues.some(issue => issue.includes('extends outside'))).toBe(true);
  });
});

describe('createBattlemapPositions cover usage', () => {
  test('some default player and enemy anchors sit on cover, but not all of them', () => {
    const layout = {
      zones: DEFAULT_ZONES,
      stations: DEFAULT_STATIONS,
      spawnPoints: DEFAULT_SPAWN_POINTS,
      cover: DEFAULT_COVER,
    };
    const players = ['p1', 'p2', 'p3', 'p4'].map(name => ({ name }));
    const enemies = ['e1', 'e2', 'e3', 'e4'].map(name => ({ name }));
    const positions = createBattlemapPositions(layout, players, enemies);

    const playerPoints = players.map(p => positions[`player:${p.name}`]);
    const enemyPoints = enemies.map(e => positions[`enemy:${e.name}`]);
    const coveredPlayers = playerPoints.filter(point => coverForPoint(layout, point));
    const coveredEnemies = enemyPoints.filter(point => coverForPoint(layout, point));

    expect(coveredPlayers.length).toBeGreaterThan(0);
    expect(coveredPlayers.length).toBeLessThan(playerPoints.length);
    expect(coveredEnemies.length).toBeGreaterThan(0);
    expect(coveredEnemies.length).toBeLessThan(enemyPoints.length);
  });
});

describe('moveWithinRange pathing', () => {
  test('routes around blockers and occupied cells on square-by-square movement', () => {
    const layout = {
      zones: [{ id: 'room', x: 0, y: 0, w: 12, h: 12 }],
      blockers: [{ id: 'wall', x: 3, y: 0, w: 1, h: 8 }],
      hazards: [],
    };
    const next = moveWithinRange(
      { x: 1.5, y: 1.5 },
      { x: 8.5, y: 1.5 },
      8,
      { width: 12, height: 12 },
      {
        layout,
        occupied: [{ x: 2.5, y: 2.5 }],
      }
    );

    expect(next.travelled).toBeLessThanOrEqual(8);
    expect(next.x).not.toBe(8.5);
    expect(next.x).not.toBe(3.5);
    expect(next.y).not.toBe(1.5);
  });
});

describe('defaultBattlemapLayout presets', () => {
  test.each([
    [{ title: 'Landing Goes Wrong' }, 'landing'],
    [{ title: 'Silent Service Corridor' }, 'corridor'],
    [{ title: 'Shrine of Black Glass' }, 'sanctum'],
    [{ title: 'Gene Vault Collapse' }, 'vault'],
    [{ title: 'Boarding Torpedo Assault' }, 'spaceship'],
    [{ title: 'Manufactorum Uprising' }, 'factory'],
    [{ title: 'Hive Street Siege' }, 'city'],
    [{ title: 'Feral Canopy Ambush' }, 'jungle'],
  ])('selects a clean, fully-connected preset for %p', (scene, expectedKey) => {
    const theme = sceneBattlemapTheme(scene);
    const layout = defaultBattlemapLayout(scene);

    expect(theme.layoutKey).toBe(expectedKey);
    expect(validateBattlemapLayout(layout.zones)).toEqual([]);
    expect(layout.zones.length).toBeGreaterThan(0);
    expect(layout.spawnPoints.length).toBeGreaterThan(0);

    // Every station and cover piece should land inside the room it's meant for.
    layout.stations.forEach((station) => {
      const zone = layout.zones.find((z) => z.id === station.zoneId);
      expect(zone && zone.x <= station.x && station.x <= zone.x + zone.w && zone.y <= station.y && station.y <= zone.y + zone.h).toBe(true);
    });
    layout.cover.forEach((item) => {
      const fits = layout.zones.some((zone) => (
        zone.x <= item.x && item.x + item.w <= zone.x + zone.w
        && zone.y <= item.y && item.y + item.h <= zone.y + zone.h
      ));
      expect(fits).toBe(true);
    });
  });
});
