import { BattlerIndex } from "#app/battle";
import { allAbilities } from "#app/data/ability";
import { Abilities } from "#app/enums/abilities";
import { WeatherType } from "#app/enums/weather-type";
import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MovePhase } from "#app/phases/move-phase";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

describe("Abilities - Forecast", () => {
  let phaserGame: Phaser.Game;
  let game: GameManager;
  const NORMAL_FORM = 0;
  const SUNNY_FORM = 1;
  const RAINY_FORM = 2;
  const SNOWY_FORM = 3;

  /**
   * Tests form changes based on weather changes
   * @param {GameManager} game The game manager instance
   * @param {WeatherType} weather The active weather to set
   * @param form The expected form based on the active weather
   * @param initialForm The initial form pre form change
   */
  const testWeatherFormChange = async (game: GameManager, weather: WeatherType, form: number, initialForm?: number) => {
    game.override.weather(weather).starterForms({ [Species.CASTFORM]: initialForm });
    await game.startBattle([ Species.CASTFORM ]);

    game.move.select(Moves.SPLASH);

    expect(game.scene.getPlayerPokemon()?.formIndex).toBe(form);
  };

  /**
   * Tests reverting to normal form when Cloud Nine/Air Lock is active on the field
   * @param {GameManager} game The game manager instance
   * @param {Abilities} ability The ability that is active on the field
   */
  const testRevertFormAgainstAbility = async (game: GameManager, ability: Abilities) => {
    game.override.starterForms({ [Species.CASTFORM]: SUNNY_FORM }).enemyAbility(ability);
    await game.startBattle([ Species.CASTFORM ]);

    game.move.select(Moves.SPLASH);

    expect(game.scene.getPlayerPokemon()?.formIndex).toBe(NORMAL_FORM);
  };

  beforeAll(() => {
    phaserGame = new Phaser.Game({
      type: Phaser.HEADLESS,
    });
  });

  afterEach(() => {
    game.phaseInterceptor.restoreOg();
  });

  beforeEach(() => {
    game = new GameManager(phaserGame);
    game.override
      .moveset([ Moves.SPLASH, Moves.RAIN_DANCE, Moves.SUNNY_DAY, Moves.TACKLE ])
      .enemySpecies(Species.MAGIKARP)
      .enemyMoveset(Moves.SPLASH)
      .enemyAbility(Abilities.BALL_FETCH);
  });

  it("changes form based on weather", async () => {
    game.override
      .moveset([ Moves.RAIN_DANCE, Moves.SUNNY_DAY, Moves.SNOWSCAPE, Moves.SPLASH ])
      .battleType("double")
      .starterForms({
        [Species.KYOGRE]: 1,
        [Species.GROUDON]: 1,
        [Species.RAYQUAZA]: 1
      });
    await game.startBattle([ Species.CASTFORM, Species.FEEBAS, Species.KYOGRE, Species.GROUDON, Species.RAYQUAZA, Species.ALTARIA ]);

    vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[Abilities.CLOUD_NINE]);

    const castform = game.scene.getPlayerField()[0];
    expect(castform.formIndex).toBe(NORMAL_FORM);

    game.move.select(Moves.RAIN_DANCE);
    game.move.select(Moves.SPLASH, 1);
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(RAINY_FORM);

    game.move.select(Moves.SUNNY_DAY);
    game.move.select(Moves.SPLASH, 1);
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(SUNNY_FORM);

    game.move.select(Moves.SNOWSCAPE);
    game.move.select(Moves.SPLASH, 1);
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(SNOWY_FORM);

    game.override.moveset([ Moves.HAIL, Moves.SANDSTORM, Moves.SNOWSCAPE, Moves.SPLASH ]);

    game.move.select(Moves.SANDSTORM);
    game.move.select(Moves.SPLASH, 1);
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(NORMAL_FORM);

    game.move.select(Moves.HAIL);
    game.move.select(Moves.SPLASH, 1);
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(SNOWY_FORM);

    game.move.select(Moves.SPLASH);
    game.doSwitchPokemon(2); // Feebas now 2, Kyogre 1
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(RAINY_FORM);

    game.move.select(Moves.SPLASH);
    game.doSwitchPokemon(3); // Kyogre now 3, Groudon 1
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(SUNNY_FORM);

    game.move.select(Moves.SPLASH);
    game.doSwitchPokemon(4); // Groudon now 4, Rayquaza 1
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(NORMAL_FORM);

    game.move.select(Moves.SPLASH);
    game.doSwitchPokemon(2); // Rayquaza now 2, Feebas 1
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(NORMAL_FORM);

    game.move.select(Moves.SNOWSCAPE);
    game.move.select(Moves.SPLASH, 1);
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(SNOWY_FORM);

    game.move.select(Moves.SPLASH);
    game.doSwitchPokemon(5); // Feebas now 5, Altaria 1
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(NORMAL_FORM);

    game.move.select(Moves.SPLASH);
    game.doSwitchPokemon(5); // Altaria now 5, Feebas 1
    await game.phaseInterceptor.to("MovePhase");
    await game.toNextTurn();

    expect(castform.formIndex).toBe(SNOWY_FORM);

    game.scene.arena.trySetWeather(WeatherType.FOG, false);
    game.move.select(Moves.SPLASH);
    game.move.select(Moves.SPLASH, 1);
    await game.phaseInterceptor.to("TurnStartPhase");

    expect(castform.formIndex).toBe(NORMAL_FORM);
  }, 30 * 1000);

  it("reverts to Normal Form during Clear weather", async () => {
    await testWeatherFormChange(game, WeatherType.NONE, NORMAL_FORM, SUNNY_FORM);
  });

  it("reverts to Normal Form if a Pokémon on the field has Air Lock", async () => {
    await testRevertFormAgainstAbility(game, Abilities.AIR_LOCK);
  });

  it("has no effect on Pokémon other than Castform", async () => {
    game.override.enemyAbility(Abilities.FORECAST).enemySpecies(Species.SHUCKLE);
    await game.startBattle([ Species.CASTFORM ]);

    game.move.select(Moves.RAIN_DANCE);
    await game.phaseInterceptor.to(TurnEndPhase);

    expect(game.scene.getPlayerPokemon()?.formIndex).toBe(RAINY_FORM);
    expect(game.scene.getEnemyPokemon()?.formIndex).not.toBe(RAINY_FORM);
  });

  it("reverts to Normal Form when Castform loses Forecast, changes form to match the weather when it regains it", async () => {
    game.override.moveset([ Moves.SKILL_SWAP, Moves.WORRY_SEED, Moves.SPLASH ]).weather(WeatherType.RAIN).battleType("double");
    await game.startBattle([ Species.CASTFORM, Species.FEEBAS ]);

    const castform = game.scene.getPlayerField()[0];

    expect(castform.formIndex).toBe(RAINY_FORM);

    game.move.select(Moves.SKILL_SWAP, 0, BattlerIndex.PLAYER_2);
    game.move.select(Moves.SKILL_SWAP, 1, BattlerIndex.PLAYER);
    await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);

    await game.phaseInterceptor.to("MoveEndPhase");
    expect(castform.formIndex).toBe(NORMAL_FORM);

    await game.phaseInterceptor.to("MoveEndPhase");
    expect(castform.formIndex).toBe(RAINY_FORM);

    await game.toNextTurn();

    game.move.select(Moves.SPLASH);
    game.move.select(Moves.WORRY_SEED, 1, BattlerIndex.PLAYER);
    await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
    await game.phaseInterceptor.to("MoveEndPhase");

    expect(castform.formIndex).toBe(NORMAL_FORM);
  });

  it("reverts to Normal Form when Forecast is suppressed, changes form to match the weather when it regains it", async () => {
    game.override.enemyMoveset([ Moves.GASTRO_ACID ]).weather(WeatherType.RAIN);
    await game.startBattle([ Species.CASTFORM, Species.PIKACHU ]);
    const castform = game.scene.getPlayerPokemon()!;

    expect(castform.formIndex).toBe(RAINY_FORM);

    // First turn - Forecast is suppressed
    game.move.select(Moves.SPLASH);
    await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
    await game.move.forceHit();

    await game.phaseInterceptor.to(TurnEndPhase);

    expect(castform.summonData.abilitySuppressed).toBe(true);
    expect(castform.formIndex).toBe(NORMAL_FORM);

    await game.toNextTurn();

    // Second turn - switch out Castform, regains Forecast
    game.doSwitchPokemon(1);
    await game.toNextTurn();

    // Third turn - switch in Castform
    game.doSwitchPokemon(1);
    await game.phaseInterceptor.to(MovePhase);

    expect(castform.summonData.abilitySuppressed).toBe(false);
    expect(castform.formIndex).toBe(RAINY_FORM);
  });

  it("does not change Castform's form until after Stealth Rock deals damage", async () => {
    game.override.weather(WeatherType.RAIN).enemyMoveset([ Moves.STEALTH_ROCK ]);
    await game.startBattle([ Species.PIKACHU, Species.CASTFORM ]);

    // First turn - set up stealth rock
    game.move.select(Moves.SPLASH);
    await game.toNextTurn();

    // Second turn - switch in Castform, regains Forecast
    game.doSwitchPokemon(1);
    await game.phaseInterceptor.to(PostSummonPhase);

    const castform = game.scene.getPlayerPokemon()!;

    // Damage phase should come first
    await game.phaseInterceptor.to(DamageAnimPhase);
    expect(castform.hp).toBeLessThan(castform.getMaxHp());

    // Then change form
    await game.phaseInterceptor.to(QuietFormChangePhase);
    expect(castform.formIndex).toBe(RAINY_FORM);
  });

  it("should be in Normal Form after the user is switched out", async () => {
    game.override.weather(WeatherType.RAIN);

    await game.startBattle([ Species.CASTFORM, Species.MAGIKARP ]);
    const castform = game.scene.getPlayerPokemon()!;

    expect(castform.formIndex).toBe(RAINY_FORM);

    game.doSwitchPokemon(1);
    await game.toNextTurn();

    expect(castform.formIndex).toBe(NORMAL_FORM);
  });
});