// Unit tests for pure game logic functions

const {
  BOARD_WIDTH,
  BOARD_HEIGHT,
  GARBAGE_COLOR,
  createEmptyBoard,
  getPieceFromType,
  isValidPosition,
  getGhostY,
  checkBoardOverflow,
  addGarbageRow,
  lockPieceToBoard,
  clearRows,
  generatePieceBag,
  createPieceQueue
} = require('../game-logic');

describe('createEmptyBoard', () => {
  test('creates a 20x10 board', () => {
    const board = createEmptyBoard();
    expect(board.length).toBe(BOARD_HEIGHT);
    expect(board[0].length).toBe(BOARD_WIDTH);
  });

  test('all cells are initialized to 0', () => {
    const board = createEmptyBoard();
    for (let row = 0; row < BOARD_HEIGHT; row++) {
      for (let col = 0; col < BOARD_WIDTH; col++) {
        expect(board[row][col]).toBe(0);
      }
    }
  });
});

describe('getPieceFromType', () => {
  test('returns correct piece for I tetromino', () => {
    const piece = getPieceFromType('I');
    expect(piece.type).toBe('I');
    expect(piece.color).toBe('#00ffff');
    expect(piece.y).toBe(0);
  });

  test('returns correct piece for O tetromino', () => {
    const piece = getPieceFromType('O');
    expect(piece.type).toBe('O');
    expect(piece.color).toBe('#ffff00');
  });

  test('returns correct piece for T tetromino', () => {
    const piece = getPieceFromType('T');
    expect(piece.type).toBe('T');
    expect(piece.color).toBe('#800080');
  });

  test('centers piece horizontally', () => {
    const piece = getPieceFromType('I');
    expect(piece.x).toBe(Math.floor(BOARD_WIDTH / 2) - Math.floor(4 / 2));
  });

  test('O piece is centered correctly', () => {
    const piece = getPieceFromType('O');
    expect(piece.x).toBe(Math.floor(BOARD_WIDTH / 2) - Math.floor(2 / 2));
  });
});

describe('isValidPosition', () => {
  test('returns true for valid position', () => {
    const board = createEmptyBoard();
    const piece = getPieceFromType('I');
    expect(isValidPosition(piece, 3, 5, board)).toBe(true);
  });

  test('returns false for position out of bounds (left)', () => {
    const board = createEmptyBoard();
    const piece = getPieceFromType('I');
    expect(isValidPosition(piece, -1, 5, board)).toBe(false);
  });

  test('returns false for position out of bounds (right)', () => {
    const board = createEmptyBoard();
    const piece = getPieceFromType('I');
    expect(isValidPosition(piece, 10, 5, board)).toBe(false);
  });

  test('returns false for position out of bounds (bottom)', () => {
    const board = createEmptyBoard();
    const piece = getPieceFromType('I');
    expect(isValidPosition(piece, 3, 20, board)).toBe(false);
  });

  test('returns false for collision with existing block', () => {
    const board = createEmptyBoard();
    board[10][5] = '#ff0000'; // Place a block
    const piece = getPieceFromType('O');
    expect(isValidPosition(piece, 4, 9, board)).toBe(false);
  });

  test('returns true for negative Y (piece spawning above board)', () => {
    const board = createEmptyBoard();
    const piece = getPieceFromType('I');
    // Negative Y is allowed for spawn area above visible board
    expect(isValidPosition(piece, 3, -1, board)).toBe(true);
  });

  test('handles piece partially above board', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('I'), y: -1 };
    // Piece can be partially above board (y < 0)
    expect(isValidPosition(piece, 3, -1, board)).toBe(true);
  });

  test('returns false when piece collides with block at spawn area', () => {
    const board = createEmptyBoard();
    board[0][3] = '#ff0000'; // Block at top
    const piece = getPieceFromType('I');
    // Piece at y=-1 would have blocks at y=-1,0,1,2 - row 0 has block
    expect(isValidPosition(piece, 3, -1, board)).toBe(false);
  });
});

describe('getGhostY', () => {
  test('returns null for no piece', () => {
    expect(getGhostY(null, createEmptyBoard())).toBe(null);
  });

  test('returns current Y when piece is at bottom', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('I'), y: 15 };
    const ghostY = getGhostY(piece, board);
    expect(ghostY).toBeLessThanOrEqual(19);
  });

  test('returns lower Y when piece can fall', () => {
    const board = createEmptyBoard();
    const piece = getPieceFromType('I');
    const ghostY = getGhostY(piece, board);
    expect(ghostY).toBeGreaterThan(piece.y);
  });

  test('calculates ghost position with obstacle', () => {
    const board = createEmptyBoard();
    board[10][3] = '#ff0000';
    board[10][4] = '#ff0000';
    board[10][5] = '#ff0000';
    board[10][6] = '#ff0000';
    const piece = getPieceFromType('I');
    const ghostY = getGhostY(piece, board);
    // I piece at x=3 with shape [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]]
    // Bottom of piece (row 1 of shape) at y=8 would hit obstacle at row 10
    expect(ghostY).toBe(8);
  });
});

describe('checkBoardOverflow', () => {
  test('returns false for empty board', () => {
    const board = createEmptyBoard();
    expect(checkBoardOverflow(board)).toBe(false);
  });

  test('returns false when only bottom rows have blocks', () => {
    const board = createEmptyBoard();
    board[19][5] = '#ff0000';
    board[18][5] = '#ff0000';
    expect(checkBoardOverflow(board)).toBe(false);
  });

  test('returns true when row 0 has a block', () => {
    const board = createEmptyBoard();
    board[0][5] = '#ff0000';
    expect(checkBoardOverflow(board)).toBe(true);
  });

  test('returns true when row 1 has a block', () => {
    const board = createEmptyBoard();
    board[1][5] = '#ff0000';
    expect(checkBoardOverflow(board)).toBe(true);
  });

  test('returns false when only row 2 has blocks', () => {
    const board = createEmptyBoard();
    board[2][5] = '#ff0000';
    expect(checkBoardOverflow(board)).toBe(false);
  });

  test('returns true when garbage fills row 0', () => {
    const board = createEmptyBoard();
    board[0].fill(GARBAGE_COLOR);
    expect(checkBoardOverflow(board)).toBe(true);
  });

  test('returns true when row 0 has garbage with gap', () => {
    const board = createEmptyBoard();
    board[0].fill(GARBAGE_COLOR);
    board[0][5] = 0; // Gap in row 0
    expect(checkBoardOverflow(board)).toBe(true); // Still overflowed
  });

  test('returns true when row 1 has garbage even if row 0 empty', () => {
    const board = createEmptyBoard();
    board[1].fill(GARBAGE_COLOR);
    // Row 0 is empty but row 1 has blocks
    expect(checkBoardOverflow(board)).toBe(true);
  });
});

describe('addGarbageRow', () => {
  test('maintains board height', () => {
    const board = createEmptyBoard();
    const result = addGarbageRow(board, null);
    expect(result.board.length).toBe(BOARD_HEIGHT);
  });

  test('adds garbage row at bottom', () => {
    const board = createEmptyBoard();
    const result = addGarbageRow(board, null);
    const bottomRow = result.board[BOARD_HEIGHT - 1];
    expect(bottomRow.some(cell => cell === GARBAGE_COLOR)).toBe(true);
  });

  test('garbage row has exactly one gap', () => {
    const board = createEmptyBoard();
    const result = addGarbageRow(board, null);
    const bottomRow = result.board[BOARD_HEIGHT - 1];
    const gaps = bottomRow.filter(cell => cell === 0).length;
    expect(gaps).toBe(1);
  });

  test('pushes current piece up by 1', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('I'), y: 5 };
    const result = addGarbageRow(board, piece);
    expect(result.currentPiece.y).toBe(4);
  });

  test('handles null current piece', () => {
    const board = createEmptyBoard();
    const result = addGarbageRow(board, null);
    expect(result.currentPiece).toBe(null);
  });

  test('garbage gap position is deterministic with mocked random', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.5; // Always return 0.5 -> gap at index 5

    const board = createEmptyBoard();
    const result = addGarbageRow(board, null);

    expect(result.gap).toBe(5);
    expect(result.board[19][5]).toBe(0);

    Math.random = originalRandom;
  });

  test('removes top row correctly', () => {
    const board = createEmptyBoard();
    board[0][5] = '#ff0000'; // Mark top row
    const result = addGarbageRow(board, null);
    // Original row 1 should now be row 0
    expect(result.board[0][5]).toBe(0);
  });

  test('piece pushed to negative Y is still tracked', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('I'), y: 2 };
    const result = addGarbageRow(board, piece);
    expect(result.currentPiece.y).toBe(1);

    // Push again
    const result2 = addGarbageRow(result.board, result.currentPiece);
    expect(result2.currentPiece.y).toBe(0);
  });
});

describe('lockPieceToBoard', () => {
  test('locks piece to board', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('O'), x: 4, y: 5 };
    const newBoard = lockPieceToBoard(piece, board);

    // O piece is 2x2, so it should fill 4 cells
    expect(newBoard[5][4]).toBe(piece.color);
    expect(newBoard[5][5]).toBe(piece.color);
    expect(newBoard[6][4]).toBe(piece.color);
    expect(newBoard[6][5]).toBe(piece.color);
  });

  test('does not modify original board', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('O'), x: 4, y: 5 };
    lockPieceToBoard(piece, board);

    // Original board should still be empty
    expect(board[5][4]).toBe(0);
  });

  test('piece at negative Y only locks visible portion', () => {
    const board = createEmptyBoard();
    const piece = {
      ...getPieceFromType('I'),
      x: 3,
      y: -1 // Top half above board
    };
    const newBoard = lockPieceToBoard(piece, board);

    // Only bottom half should be locked (I piece is 4 rows tall)
    expect(newBoard[0][3]).toBe(piece.color);
    expect(newBoard[0][4]).toBe(piece.color);
    expect(newBoard[0][5]).toBe(piece.color);
    expect(newBoard[0][6]).toBe(piece.color);
  });

  test('piece at bottom of board locks correctly', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('O'), x: 4, y: 18 };
    const newBoard = lockPieceToBoard(piece, board);

    expect(newBoard[18][4]).toBe(piece.color);
    expect(newBoard[18][5]).toBe(piece.color);
    expect(newBoard[19][4]).toBe(piece.color);
    expect(newBoard[19][5]).toBe(piece.color);
  });

  test('piece partially off bottom edge only locks visible part', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('I'), x: 3, y: 16 };
    const newBoard = lockPieceToBoard(piece, board);

    // I piece shape: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]]
    // At y=16, the blocks (row 1 of shape) lock at row 17
    // Rows 17,18,19 are visible
    expect(newBoard[17][3]).toBe(piece.color);
    expect(newBoard[17][4]).toBe(piece.color);
    expect(newBoard[17][5]).toBe(piece.color);
    expect(newBoard[17][6]).toBe(piece.color);
  });

  test('piece completely above board locks nothing', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('I'), x: 3, y: -10 };
    const newBoard = lockPieceToBoard(piece, board);

    // Board should still be empty
    expect(newBoard[0][3]).toBe(0);
  });

  test('piece at left edge locks correctly', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('O'), x: 0, y: 5 };
    const newBoard = lockPieceToBoard(piece, board);

    expect(newBoard[5][0]).toBe(piece.color);
    expect(newBoard[5][1]).toBe(piece.color);
  });

  test('piece at right edge locks correctly', () => {
    const board = createEmptyBoard();
    const piece = { ...getPieceFromType('O'), x: 8, y: 5 };
    const newBoard = lockPieceToBoard(piece, board);

    expect(newBoard[5][8]).toBe(piece.color);
    expect(newBoard[5][9]).toBe(piece.color);
  });
});

describe('clearRows', () => {
  test('returns 0 rows cleared for empty board', () => {
    const board = createEmptyBoard();
    const result = clearRows(board);
    expect(result.rowsCleared).toBe(0);
  });

  test('clears one complete row', () => {
    const board = createEmptyBoard();
    board[10].fill('#ff0000'); // Fill row 10
    const result = clearRows(board);
    expect(result.rowsCleared).toBe(1);
  });

  test('clears multiple complete rows', () => {
    const board = createEmptyBoard();
    board[10].fill('#ff0000');
    board[11].fill('#00ff00');
    board[12].fill('#0000ff');
    const result = clearRows(board);
    expect(result.rowsCleared).toBe(3);
  });

  test('adds empty rows at top after clearing', () => {
    const board = createEmptyBoard();
    board[19].fill('#ff0000'); // Fill bottom row
    const result = clearRows(board);
    expect(result.board[0].every(cell => cell === 0)).toBe(true);
  });

  test('maintains board height after clearing', () => {
    const board = createEmptyBoard();
    board[18].fill('#ff0000');
    board[19].fill('#00ff00');
    const result = clearRows(board);
    expect(result.board.length).toBe(BOARD_HEIGHT);
  });

  test('does not clear incomplete rows', () => {
    const board = createEmptyBoard();
    board[10].fill('#ff0000');
    board[10][5] = 0; // Leave a gap
    const result = clearRows(board);
    expect(result.rowsCleared).toBe(0);
  });

  test('clears tetris (4 rows at once)', () => {
    const board = createEmptyBoard();
    board[16].fill('#ff0000');
    board[17].fill('#ff0000');
    board[18].fill('#ff0000');
    board[19].fill('#ff0000');
    const result = clearRows(board);
    expect(result.rowsCleared).toBe(4);
  });

  test('clears rows from bottom to top', () => {
    const board = createEmptyBoard();
    board[5].fill('#ff0000');
    board[10].fill('#00ff00');
    board[15].fill('#0000ff');
    const result = clearRows(board);
    expect(result.rowsCleared).toBe(3);
  });

  test('shifts rows down correctly after clearing', () => {
    const board = createEmptyBoard();
    board[18].fill('#ff0000');
    board[19].fill('#00ff00');
    const result = clearRows(board);
    // After clearing 2 rows, top should be empty
    expect(result.board[0].every(cell => cell === 0)).toBe(true);
    expect(result.board[1].every(cell => cell === 0)).toBe(true);
  });
});

describe('generatePieceBag', () => {
  test('returns array of 7 pieces', () => {
    const bag = generatePieceBag();
    expect(bag.length).toBe(7);
  });

  test('contains all 7 tetromino types', () => {
    const bag = generatePieceBag();
    const types = new Set(bag);
    expect(types.size).toBe(7);
    expect(types.has('I')).toBe(true);
    expect(types.has('O')).toBe(true);
    expect(types.has('T')).toBe(true);
    expect(types.has('S')).toBe(true);
    expect(types.has('Z')).toBe(true);
    expect(types.has('J')).toBe(true);
    expect(types.has('L')).toBe(true);
  });

  test('each bag contains exactly one of each piece type', () => {
    const bag = generatePieceBag();
    const counts = {};
    bag.forEach(type => counts[type] = (counts[type] || 0) + 1);

    expect(counts.I).toBe(1);
    expect(counts.O).toBe(1);
    expect(counts.T).toBe(1);
    expect(counts.S).toBe(1);
    expect(counts.Z).toBe(1);
    expect(counts.J).toBe(1);
    expect(counts.L).toBe(1);
  });
});

describe('createPieceQueue', () => {
  test('creates queue with correct length', () => {
    const queue = createPieceQueue(2);
    expect(queue.length).toBe(14); // 2 bags * 7 pieces
  });

  test('uses default bag count of 14', () => {
    const queue = createPieceQueue();
    expect(queue.length).toBe(98); // 14 bags * 7 pieces
  });

  test('creates multiple complete bags', () => {
    const queue = createPieceQueue(3); // 3 bags = 21 pieces
    expect(queue.length).toBe(21);

    // Each 7-piece segment should have all types
    for (let i = 0; i < 3; i++) {
      const bag = queue.slice(i * 7, (i + 1) * 7);
      expect(new Set(bag).size).toBe(7);
    }
  });
});

describe('garbage edge cases', () => {
  test('multiple garbage rows push piece progressively', () => {
    let board = createEmptyBoard();
    let piece = { ...getPieceFromType('I'), y: 10 };

    // Add 5 garbage rows
    for (let i = 0; i < 5; i++) {
      const result = addGarbageRow(board, piece);
      board = result.board;
      piece = result.currentPiece;
    }

    expect(piece.y).toBe(5); // 10 - 5 = 5
  });

  test('19 garbage rows causes board overflow', () => {
    let board = createEmptyBoard();
    let piece = { ...getPieceFromType('I'), y: 10 };

    for (let i = 0; i < 19; i++) {
      const result = addGarbageRow(board, piece);
      board = result.board;
      piece = result.currentPiece;
    }

    // Board should now have garbage in top rows (row 0 and 1)
    expect(checkBoardOverflow(board)).toBe(true);
  });

  test('piece pushed to negative Y is still tracked correctly', () => {
    let board = createEmptyBoard();
    let piece = { ...getPieceFromType('I'), y: 5 };

    // Push piece 10 rows up
    for (let i = 0; i < 10; i++) {
      const result = addGarbageRow(board, piece);
      board = result.board;
      piece = result.currentPiece;
    }

    expect(piece.y).toBe(-5);
    // Board not overflowed yet (piece is above board, not in it)
    expect(checkBoardOverflow(board)).toBe(false);
  });
});

describe('performance', () => {
  test('checkBoardOverflow is fast', () => {
    const board = createEmptyBoard();

    expect(() => {
      for (let i = 0; i < 10000; i++) {
        checkBoardOverflow(board);
      }
    }).not.toThrow();
  });

  test('createEmptyBoard is fast', () => {
    expect(() => {
      for (let i = 0; i < 1000; i++) {
        createEmptyBoard();
      }
    }).not.toThrow();
  });
});
