import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DungeonRenderer } from '../DungeonRenderer';
import { type World } from '../../core/types';

// Mock Phaser
vi.mock('phaser', () => {
  const mockSprite = {
    setDepth: vi.fn().mockReturnThis(),
    setScale: vi.fn().mockReturnThis(),
    play: vi.fn().mockReturnThis(),
    setPosition: vi.fn().mockReturnThis(),
    setVisible: vi.fn().mockReturnThis(),
    destroy: vi.fn(),
  };

  const mockGraphics = {
    clear: vi.fn().mockReturnThis(),
    fillStyle: vi.fn().mockReturnThis(),
    fillRect: vi.fn().mockReturnThis(),
    lineStyle: vi.fn().mockReturnThis(),
    strokeRect: vi.fn().mockReturnThis(),
  };

  const mockContainer = {
    add: vi.fn().mockReturnThis(),
    setPosition: vi.fn().mockReturnThis(),
    setVisible: vi.fn().mockReturnThis(),
    setScrollFactor: vi.fn().mockReturnThis(),
    setDepth: vi.fn().mockReturnThis(),
  };

  const mockRectangle = {
    setStrokeStyle: vi.fn().mockReturnThis(),
    setInteractive: vi.fn().mockReturnThis(),
  };

  return {
    default: {
      GameObjects: {
        Sprite: vi.fn(() => mockSprite),
        Graphics: vi.fn(() => mockGraphics),
        Container: vi.fn(() => mockContainer),
        Rectangle: vi.fn(() => mockRectangle),
      },
      Scene: vi.fn(),
      Math: {
        Clamp: vi.fn((v, min, max) => Math.min(Math.max(v, min), max)),
      },
    },
  };
});

describe('DungeonRenderer', () => {
  let mockScene: any;
  let renderer: DungeonRenderer;
  let mockWorld: World;

  beforeEach(() => {
    vi.clearAllMocks();

    mockScene = {
      add: {
        graphics: vi.fn().mockReturnValue({
          clear: vi.fn(),
          fillStyle: vi.fn(),
          fillRect: vi.fn(),
        }),
        sprite: vi.fn(() => ({
          setDepth: vi.fn().mockReturnThis(),
          setScale: vi.fn().mockReturnThis(),
          play: vi.fn().mockReturnThis(),
          setPosition: vi.fn().mockReturnThis(),
          setVisible: vi.fn().mockReturnThis(),
          destroy: vi.fn(),
        })),
        container: vi.fn().mockReturnValue({
          add: vi.fn(),
          setPosition: vi.fn(),
          setVisible: vi.fn(),
          setScrollFactor: vi.fn(),
          setDepth: vi.fn(),
        }),
        rectangle: vi.fn().mockReturnValue({
          setStrokeStyle: vi.fn().mockReturnThis(),
          setInteractive: vi.fn().mockReturnThis(),
        }),
      },
      cameras: {
        main: {
          width: 800,
          height: 600,
        },
      },
      anims: {
        create: vi.fn(),
        exists: vi.fn().mockReturnValue(true),
        generateFrameNumbers: vi.fn(),
      },
      make: {
        tilemap: vi.fn().mockReturnValue({
          addTilesetImage: vi.fn().mockReturnValue({}),
          createLayer: vi.fn().mockReturnValue({
            setDepth: vi.fn(),
            forEachTile: vi.fn(),
          }),
          destroy: vi.fn(),
        }),
      },
      tweens: {
        add: vi.fn(),
        killTweensOf: vi.fn(),
      },
    };


    mockWorld = {
      width: 10,
      height: 10,
      tiles: new Array(100).fill(0),
      actors: new Map(),
      exit: { x: 9, y: 9 },
    };

    renderer = new DungeonRenderer(mockScene);
  });

  it('should track and clear corpse sprites on floor initialization', () => {
    renderer.initializeFloor(mockWorld, 1);


    // Spawn a couple of corpses
    renderer.spawnCorpse(1, 1, 'rat');
    renderer.spawnCorpse(2, 2, 'bat');

    // Get the mock sprites that were returned by scene.add.sprite
    const corpse1 = mockScene.add.sprite.mock.results[1].value;
    const corpse2 = mockScene.add.sprite.mock.results[2].value;

    expect(mockScene.add.sprite).toHaveBeenCalledTimes(3);

    // Initialize floor again (changing level)
    renderer.initializeFloor(mockWorld, 1);


    // Verify destroy was called on both corpse sprites
    expect(corpse1.destroy).toHaveBeenCalledTimes(1);
    expect(corpse2.destroy).toHaveBeenCalledTimes(1);
  });

  it('should render exp_orb as a circle and not as an enemy sprite', () => {
    renderer.initializeFloor(mockWorld, 1);
    
    // Add an exp_orb to the world
    mockWorld.actors.set(2, {
      id: 2,
      category: "collectible",
      type: "exp_orb",
      pos: { x: 2, y: 1 },
      expAmount: 10
    });

    // Make the tile visible for it to render
    (renderer as any).fovManager.visibleArray[1 * mockWorld.width + 2] = 1;

    // Reset mocks
    mockScene.add.sprite.mockClear();

    // Mock scene.add.circle
    mockScene.add.circle = vi.fn().mockReturnValue({
      setStrokeStyle: vi.fn().mockReturnThis(),
      setDepth: vi.fn().mockReturnThis(),
      setPosition: vi.fn().mockReturnThis(),
      setVisible: vi.fn().mockReturnThis(),
    });

    renderer.render([]);

    // Should NOT have added an enemy sprite for the orb
    const spriteCalls = mockScene.add.sprite.mock.calls;
    // Any sprite added that isn't the player (which isn't in mockWorld.actors here except if we added it)
    // The current loop skips a.isPlayer and then checks if type is in GAME_CONFIG.enemies
    expect(spriteCalls.length).toBe(0);

    // Should HAVE added a circle for the orb
    expect(mockScene.add.circle).toHaveBeenCalled();
  });

  it('should render any enemy type defined in config as a sprite', () => {
    renderer.initializeFloor(mockWorld, 1);
    
    // Add a rat (defined in config)
    mockWorld.actors.set(3, {
      id: 3,
      category: "combatant",
      isPlayer: false,
      type: "rat",
      pos: { x: 3, y: 1 },
      speed: 10,
      stats: { hp: 10, maxHp: 10, attack: 2, defense: 0 } as any,
      energy: 10
    });

    (renderer as any).fovManager.visibleArray[1 * mockWorld.width + 3] = 1;
    mockScene.add.sprite.mockClear();

    renderer.render([]);

    // Should have added a sprite for the rat
    const ratSpriteCall = mockScene.add.sprite.mock.calls.find((call: any) => call[2] === 'rat');
    expect(ratSpriteCall).toBeDefined();
  });

  it('should initialize new enemy sprites at target position and not tween them', () => {
    renderer.initializeFloor(mockWorld, 1);
    
    // Position 5,5 -> 5*16 + 8 = 88
    const TILE_SIZE = 16;
    const targetX = 5 * TILE_SIZE + TILE_SIZE / 2;
    const targetY = 5 * TILE_SIZE + TILE_SIZE / 2;

    mockWorld.actors.set(999, {
      id: 999,
      category: "combatant",
      isPlayer: false,
      type: "rat",
      pos: { x: 5, y: 5 },
      stats: { hp: 10, maxHp: 10 } as any,
    } as any);

    (renderer as any).fovManager.visibleArray[5 * mockWorld.width + 5] = 1;
    mockScene.add.sprite.mockClear();
    mockScene.tweens.add.mockClear();

    renderer.render([]);

    // Check spawn position
    expect(mockScene.add.sprite).toHaveBeenCalledWith(targetX, targetY, 'rat', 0);
    
    // Should NOT tween because it's the first spawn
    expect(mockScene.tweens.add).not.toHaveBeenCalled();
  });
});
