import { describe, expect, it } from 'vitest';
import { buildScoresheet, layoutImage, formatFinishedDate, MIN_SIDE } from './gameimage';
import type { GameView, MoveRecord, Seat } from './model';

function seat(n: number, name: string, score: number, winner = false): Seat {
  return { seat: n, accountId: `a${n}`, displayName: name, score, hintsUsed: 0, isWinner: winner };
}

function game(seats: Seat[], over = true): GameView {
  return {
    id: 'g1',
    variant: 'scrabble_ru',
    dictVersion: 'v1',
    vsAi: false,
    unreadChat: false,
    unreadMessages: false,
    kind: 0,
    status: over ? 'finished' : 'active',
    players: seats.length,
    toMove: 0,
    turnTimeoutSecs: 300,
    multipleWordsPerTurn: false,
    noRepeatWords: false,
    moveCount: 0,
    endReason: over ? 'standard' : '',
    lastActivityUnix: 1_782_997_629, // 2026-07-02 (UTC)
    seats,
  };
}

function play(player: number, words: string[], score: number, total: number, dir: 'H' | 'V' = 'H'): MoveRecord {
  return { player, action: 'play', dir, mainRow: 7, mainCol: 7, tiles: [], words, count: words.length, score, total };
}

// A non-play move carries the seat's current running total on the wire (engine game.go
// sets Total: g.scores[player] on pass/exchange/resign) — the fixture mirrors that.
function action(player: number, act: 'pass' | 'exchange' | 'resign', total = 0): MoveRecord {
  return { player, action: act, dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total };
}

const label = (a: string) => `<${a}>`;

describe('buildScoresheet', () => {
  it('builds per-seat header, coordinated play rows and localized actions', () => {
    const g = game([seat(0, 'Аня', 118, true), seat(1, 'Боб', 92)]);
    const moves = [
      play(0, ['ГРОТ'], 24, 24),
      action(1, 'exchange'),
      play(0, ['МЫС'], 18, 42, 'V'),
      play(1, ['СОН'], 12, 12),
    ];
    const s = buildScoresheet(g, moves, label, { hostname: 'erudit-game.ru', dateLocale: 'ru' });

    expect(s.header).toEqual([
      { name: 'Аня', score: 118, winner: true },
      { name: 'Боб', score: 92, winner: false },
    ]);
    expect(s.rows).toHaveLength(2);
    expect(s.rows[0][0]).toMatchObject({ kind: 'play', coord: '8H', words: ['ГРОТ'], score: 24 });
    expect(s.rows[0][1]).toMatchObject({ kind: 'action', label: '<exchange>' });
    expect(s.rows[1][0]).toMatchObject({ kind: 'play', coord: 'H8' }); // vertical → letter first
    expect(s.rows[1][1]).toMatchObject({ kind: 'play', coord: '8H', words: ['СОН'] });
    expect(s.footer.startsWith('erudit-game.ru · ')).toBe(true);
    expect(s.footer).toContain('2026');
  });

  it('derives the endgame rack settlement from final scores vs last running totals', () => {
    // Seat 0 went out (+6 from the opponent rack), seat 1 lost its rack value (−6).
    const g = game([seat(0, 'A', 48, true), seat(1, 'B', 24)]);
    const moves = [play(0, ['X'], 42, 42), play(1, ['Y'], 30, 30)];
    const s = buildScoresheet(g, moves, label, { hostname: 'h' });
    expect(s.adjustments).toEqual([6, -6]);
    expect(s.hasAdjustments).toBe(true);
  });

  it('reports no settlement when the final scores match the journal (resignation)', () => {
    const g = game([seat(0, 'A', 42, true), seat(1, 'B', 30)]);
    const moves = [play(0, ['X'], 42, 42), play(1, ['Y'], 30, 30), action(1, 'resign', 30)];
    const s = buildScoresheet(g, moves, label, { hostname: 'h' });
    expect(s.adjustments).toEqual([0, 0]);
    expect(s.hasAdjustments).toBe(false);
  });

  it('keeps one column per seat for a four-player game', () => {
    const g = game([seat(0, 'A', 10), seat(1, 'B', 20, true), seat(2, 'C', 5), seat(3, 'D', 0)]);
    const moves = [play(0, ['X'], 10, 10), play(1, ['Y'], 20, 20), play(2, ['Z'], 5, 5)];
    const s = buildScoresheet(g, moves, label, { hostname: 'h' });
    expect(s.header).toHaveLength(4);
    expect(s.rows[0]).toHaveLength(4);
    expect(s.rows[0][3]).toEqual({ kind: 'empty' });
  });
});

describe('layoutImage', () => {
  const short = (seats = 2) =>
    buildScoresheet(
      game(Array.from({ length: seats }, (_, i) => seat(i, `P${i}`, 0))),
      [play(0, ['ГРОТ'], 24, 24)],
      label,
      { hostname: 'h' },
    );

  it('clamps the board to MIN_SIDE for a short game', () => {
    const l = layoutImage(short());
    expect(l.board.side).toBe(MIN_SIDE);
    expect(l.board.cell).toBeCloseTo(MIN_SIDE / 15);
  });

  it('stretches the board (and the image) to a long scoresheet, keeping the typography fixed', () => {
    const moves: MoveRecord[] = [];
    for (let i = 0; i < 80; i++) moves.push(play(i % 2, ['СЛОВО'], 10, 10 * i));
    const s = buildScoresheet(game([seat(0, 'A', 300, true), seat(1, 'B', 290)]), moves, label, { hostname: 'h' });
    const l = layoutImage(s);
    expect(l.board.side).toBeGreaterThan(MIN_SIDE);
    // The column content exactly fills the board height (that is the stretch contract).
    const content =
      l.headNameH + l.headScoreH + l.headRuleH + l.rowHeights.reduce((a, b) => a + b, 0) + l.adjRowH;
    expect(l.board.side).toBe(Math.ceil(content));
    expect(l.h).toBe(l.margin + l.axis + l.board.side + l.footerH + l.margin);
  });

  it('gives a row the extra sub-line only when a play formed several words', () => {
    const s = buildScoresheet(
      game([seat(0, 'A', 0), seat(1, 'B', 0)]),
      [play(0, ['ОСА', 'ОМ'], 12, 12), play(1, ['ДОМ'], 8, 8)],
      label,
      { hostname: 'h' },
    );
    const l = layoutImage(s);
    expect(l.rowHeights[0]).toBe(l.rowH + l.subRowH);
    expect(l.rowHeights).toHaveLength(1);
  });

  it('widens the sheet per seat', () => {
    const two = layoutImage(short(2));
    const four = layoutImage(short(4));
    expect(four.hist.w).toBeGreaterThan(two.hist.w);
    expect(four.w - four.hist.w).toBe(two.w - two.hist.w); // board block unchanged
  });
});

describe('formatFinishedDate', () => {
  it('formats with the given locale (regional convention, long month)', () => {
    const s = formatFinishedDate(1_782_997_629, 'ru');
    expect(s).toContain('2026');
    expect(s).toMatch(/июн|июл/); // TZ-dependent day, but always mid-summer
  });
});
