import { describe, expect, it } from 'vitest';
import {
  GAME_STEPS,
  LOBBY_STEPS,
  nextVisibleStep,
  placeBubble,
  stepsFor,
  type Rect,
  type Viewport,
} from './coachmark';

describe('stepsFor', () => {
  it('returns the owner-defined order for each series', () => {
    expect(stepsFor('lobby')).toBe(LOBBY_STEPS);
    expect(stepsFor('game')).toBe(GAME_STEPS);
    expect(LOBBY_STEPS.map((s) => s.target)).toEqual(['lobby-settings', 'lobby-stats', 'lobby-new']);
    expect(GAME_STEPS.map((s) => s.target)).toEqual(['game-header', 'game-turn', 'game-hints', 'game-shuffle', 'game-rack']);
  });
});

describe('nextVisibleStep', () => {
  const present = () => true;
  it('returns the start index when its target is present', () => {
    expect(nextVisibleStep(LOBBY_STEPS, 0, present)).toBe(0);
    expect(nextVisibleStep(LOBBY_STEPS, 1, present)).toBe(1);
  });

  it('skips steps whose target is absent', () => {
    const isPresent = (t: string) => t !== 'game-hints' && t !== 'game-shuffle';
    // From the hints step (index 2), both hints and shuffle are absent, so it lands on the rack.
    expect(nextVisibleStep(GAME_STEPS, 2, isPresent)).toBe(4);
  });

  it('returns -1 when no further step is visible', () => {
    expect(nextVisibleStep(GAME_STEPS, 5, present)).toBe(-1);
    expect(nextVisibleStep(GAME_STEPS, 0, () => false)).toBe(-1);
  });
});

describe('placeBubble', () => {
  const portrait: Viewport = { width: 390, height: 844 };
  const bubble = { width: 280, height: 80 };

  it('places the bubble below a target at the top (scoreboard strip)', () => {
    const target: Rect = { left: 0, top: 50, width: 390, height: 40 };
    const p = placeBubble(target, portrait, bubble);
    expect(p.side).toBe('bottom');
    expect(p.top).toBe(100); // 50 + 40 + gap(10)
    expect(p.left).toBe(55); // centred under the target, room to spare
    expect(p.tail).toBeGreaterThan(8);
  });

  it('places the bubble above a bottom tab-bar target and clamps it on screen', () => {
    const target: Rect = { left: 260, top: 790, width: 120, height: 54 };
    const p = placeBubble(target, portrait, bubble);
    expect(p.side).toBe('top');
    expect(p.top).toBe(700); // 790 - gap(10) - height(80)
    // Centring would overflow the right edge, so the bubble is clamped...
    expect(p.left).toBe(portrait.width - bubble.width - 8);
    // ...yet the tail still aims at the target centre.
    const tailViewportX = p.left + p.tail;
    expect(Math.abs(tailViewportX - (target.left + target.width / 2))).toBeLessThan(1);
  });

  it('places the bubble to the side of a tall left-panel target (landscape)', () => {
    const landscape: Viewport = { width: 1100, height: 640 };
    const wide = { width: 300, height: 120 };
    const target: Rect = { left: 30, top: 20, width: 60, height: 600 };
    const p = placeBubble(target, landscape, wide);
    expect(p.side).toBe('right');
    expect(p.left).toBe(100); // 30 + 60 + gap(10)
  });

  it('clamps the bubble and the tail at a corner target', () => {
    const target: Rect = { left: 0, top: 0, width: 30, height: 30 };
    const p = placeBubble(target, portrait, { width: 200, height: 60 });
    expect(p.side).toBe('bottom');
    expect(p.left).toBe(8); // left margin
    expect(p.tail).toBe(8); // tail clamped to its minimum off the corner
  });

  it('falls back to a side and keeps the bubble within the viewport when nothing fits', () => {
    const target: Rect = { left: 0, top: 0, width: 390, height: 844 };
    const p = placeBubble(target, portrait, { width: 200, height: 80 });
    expect(p.left).toBeGreaterThanOrEqual(8);
    expect(p.left).toBeLessThanOrEqual(portrait.width - 200 - 8);
    expect(p.top).toBeGreaterThanOrEqual(8);
    expect(p.top).toBeLessThanOrEqual(portrait.height - 80 - 8);
  });
});
