import {describe, test, expect, vi} from 'vitest';
import {InViewTiles} from './tile_manager_in_view_tiles.ts';
import {Tile} from './tile.ts';
import {OverscaledTileID} from './tile_id.ts';
import type {Painter} from '../render/painter.ts';

describe('InViewTiles', () => {
    test('getRenderableIds returns only renderable tiles', () => {
        const inViewTiles = new InViewTiles();
        const tile1 = new Tile(new OverscaledTileID(0, 0, 0, 0, 0), 512);
        tile1.state = 'loaded'; // renderable
        const tile2 = new Tile(new OverscaledTileID(1, 0, 0, 0, 0), 512);
        tile2.state = 'loading'; // not renderable

        inViewTiles.setTile(tile1.tileID.key, tile1);
        inViewTiles.setTile(tile2.tileID.key, tile2);

        const renderableIds = inViewTiles.getRenderableIds();
        expect(renderableIds).toEqual([tile1.tileID.key]);
    });

    test('getRenderableIds sorts by tile ID when symbolLayer is false', () => {
        const inViewTiles = new InViewTiles();
        const tile1 = new Tile(new OverscaledTileID(1, 0, 1, 1, 0), 512);
        tile1.state = 'loaded';
        const tile2 = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512);
        tile2.state = 'loaded';

        inViewTiles.setTile(tile1.tileID.key, tile1);
        inViewTiles.setTile(tile2.tileID.key, tile2);

        const renderableIds = inViewTiles.getRenderableIds();
        // compareTileId sorts by z (asc), then y (desc), then x (desc).
        // tile1: z=1, x=1, y=0.
        // tile2: z=1, x=0, y=0.
        // tile1.x > tile2.x, so tile1 comes first.
        expect(renderableIds).toEqual([tile1.tileID.key, tile2.tileID.key]);
    });

    test('getRenderableIds sorts by bearing (0 degrees) and coordinates when symbolLayer is true', () => {
        const inViewTiles = new InViewTiles();
        
        // Same Z, different Y
        const tile1 = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512); // 0,0
        tile1.state = 'loaded';
        const tile2 = new Tile(new OverscaledTileID(1, 0, 1, 0, 1), 512); // 0,1 (below tile1)
        tile2.state = 'loaded';

        inViewTiles.setTile(tile1.tileID.key, tile1);
        inViewTiles.setTile(tile2.tileID.key, tile2);

        // Case 1: Bearing 0.
        // rotatedB.y (1) - rotatedA.y (0) = 1 > 0. b comes first.
        const renderableIds = inViewTiles.getRenderableIds(0, true);
        expect(renderableIds).toEqual([tile2.tileID.key, tile1.tileID.key]);
    });

    test('getRenderableIds sorts by bearing (180 degrees) and coordinates when symbolLayer is true', () => {
        const inViewTiles = new InViewTiles();
        
        // Same Z, different Y
        const tile1 = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512); // 0,0
        tile1.state = 'loaded';
        const tile2 = new Tile(new OverscaledTileID(1, 0, 1, 0, 1), 512); // 0,1 (below tile1)
        tile2.state = 'loaded';

        inViewTiles.setTile(tile1.tileID.key, tile1);
        inViewTiles.setTile(tile2.tileID.key, tile2);

        // Case 2: Bearing 180 degrees (PI).
        // rotatedB.y (-1) - rotatedA.y (0) = -1 < 0. a comes first.
        const renderableIds = inViewTiles.getRenderableIds(Math.PI, true);
        expect(renderableIds).toEqual([tile1.tileID.key, tile2.tileID.key]);
    });

    test('handleWrapJump updates tile IDs with correct wrap', () => {
        const inViewTiles = new InViewTiles();
        const tile = new Tile(new OverscaledTileID(0, 0, 0, 0, 0), 512);
        inViewTiles.setTile(tile.tileID.key, tile);

        inViewTiles.handleWrapJump(1);

        const tiles = inViewTiles.getAllTiles();
        expect(tiles).toHaveLength(1);
        expect(tiles[0].tileID.wrap).toBe(1);
        expect(inViewTiles.getTileById(tiles[0].tileID.key)).toBe(tiles[0]);
    });

    test('handleWrapJump updates multiple tiles correctly', () => {
        const inViewTiles = new InViewTiles();
        const tile1 = new Tile(new OverscaledTileID(0, 0, 0, 0, 0), 512);
        const tile2 = new Tile(new OverscaledTileID(0, 1, 0, 0, 0), 512);
        inViewTiles.setTile(tile1.tileID.key, tile1);
        inViewTiles.setTile(tile2.tileID.key, tile2);

        inViewTiles.handleWrapJump(-1);

        const tiles = inViewTiles.getAllTiles();
        expect(tiles).toHaveLength(2);
        
        const updatedTile1 = tiles.find(t => t.uid === tile1.uid);
        const updatedTile2 = tiles.find(t => t.uid === tile2.uid);

        expect(updatedTile1?.tileID.wrap).toBe(-1);
        expect(updatedTile2?.tileID.wrap).toBe(0);
        
        expect(inViewTiles.getTileById(updatedTile1.tileID.key)).toBe(updatedTile1);
        expect(inViewTiles.getTileById(updatedTile2.tileID.key)).toBe(updatedTile2);
    });

    test('setFeatureState calls setFeatureState on each tile', () => {
        const inViewTiles = new InViewTiles();
        const tile1 = new Tile(new OverscaledTileID(0, 0, 0, 0, 0), 512);
        const tile2 = new Tile(new OverscaledTileID(1, 0, 1, 0, 0), 512);
        const spy1 = vi.spyOn(tile1, 'setFeatureState').mockImplementation(() => {});
        const spy2 = vi.spyOn(tile2, 'setFeatureState').mockImplementation(() => {});

        inViewTiles.setTile(tile1.tileID.key, tile1);
        inViewTiles.setTile(tile2.tileID.key, tile2);

        const states = {road: [{id: '1', state: {hover: true}}]};
        const painter = {style: {}} as unknown as Painter;
        const revision = 3;

        inViewTiles.setFeatureState(states, painter, revision);

        expect(spy1).toHaveBeenCalledWith(states, painter, revision);
        expect(spy2).toHaveBeenCalledWith(states, painter, revision);
    });
});
