import { describe, it, expect, beforeEach, vi } from 'vitest';
import { processCombatTick, makeInitialSpells } from '../stores/combat-actions';
import { useCombatStore, makeInitialSpells as makeStoreInitialSpells } from '../stores/combatStore';
import { useManaStore, makeInitialElements } from '../stores/manaStore';
import { useGameStore } from '../stores/gameStore';
import { usePrestigeStore } from '../stores/prestigeStore';
import { useUIStore } from '../stores/uiStore';
import { useDisciplineStore } from '../stores/discipline-slice';
import { getFloorMaxHP } from '../utils';
import type { CombatTickResult } from '../stores/combat-actions';

// ─── Helpers ──────────────────────────────────────────────────────────────────

function resetStores() {
  useUIStore.setState({ paused: false, gameOver: false, victory: false, logs: [] });
  useGameStore.setState({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: true });
  useManaStore.setState({ rawMana: 1000, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(500, {}) });
  useCombatStore.setState({
    currentFloor: 1,
    floorHP: getFloorMaxHP(1),
    floorMaxHP: getFloorMaxHP(1),
    maxFloorReached: 1,
    activeSpell: 'manaBolt',
    currentAction: 'climb',
    castProgress: 0,
    spireMode: false,
    currentRoom: { roomType: 'combat', enemies: [{ id: 'enemy', name: 'Test Enemy', hp: getFloorMaxHP(1), maxHP: getFloorMaxHP(1), armor: 0, dodgeChance: 0, barrier: 0, element: 'fire', activeEffects: [], effectiveArmor: 0 }] },
    clearedFloors: {},
    climbDirection: 'up',
    isDescending: false,
    startFloor: 1,
    exitFloor: 1,
    currentRoomIndex: 0,
    roomsPerFloor: 5,
    descentPeak: null,
    roomResetState: {},
    clearedRooms: {},
    isDescentComplete: false,
    golemancy: { activeGolems: [], lastSummonFloor: 0, golemDesigns: {}, golemLoadout: [] },
    equipmentSpellStates: [],
    comboHitCount: 0,
    floorHitCount: 0,
    spells: makeStoreInitialSpells(),
    activityLog: [],
    achievements: { unlocked: [], progress: {} },
    totalSpellsCast: 0,
    totalDamageDealt: 0,
    totalCraftsCompleted: 0,
  });
  usePrestigeStore.setState({
    loopCount: 0, insight: 0, totalInsight: 0, loopInsight: 0,
    prestigeUpgrades: {}, pactSlots: 1, defeatedGuardians: [],
    signedPacts: [], signedPactDetails: {},
    pactRitualFloor: null, pactRitualProgress: 0,
  });
  useDisciplineStore.setState({ disciplines: {}, activeIds: [], concurrentLimit: 1, totalXP: 0, processedPerks: [] });
}

function golemApplyDamageToRoom(dmg: number): { floorHP: number; floorMaxHP: number; roomCleared: boolean } {
  const cs = useCombatStore.getState();
  const newFloorHP = Math.max(0, cs.floorHP - dmg);
  return { floorHP: newFloorHP, floorMaxHP: cs.floorMaxHP, roomCleared: newFloorHP <= 0 };
}

function runCombatTick(rawMana: number, elements: Record<string, { current: number; max: number; unlocked: boolean }>): CombatTickResult {
  return processCombatTick(
    () => useCombatStore.getState(),
    (partial) => useCombatStore.setState(partial),
    rawMana,
    elements,
    1000, // maxMana
    1,    // attackSpeedMult
    vi.fn(), // onFloorCleared
    (dmg) => ({ rawMana, elements, modifiedDamage: dmg }), // onDamageDealt (no modifiers)
    [],   // signedPacts
    { activeGolems: [] }, // golemancyState
    golemApplyDamageToRoom,
    (dmg: number) => dmg, // applyEnemyDefenses (passthrough for tests)
  );
}

function processCombatTickDirect(
  get: () => ReturnType<typeof useCombatStore.getState>,
  set: (partial: Record<string, unknown>) => void,
  rawMana: number,
  elements: Record<string, { current: number; max: number; unlocked: boolean }>,
  maxMana: number,
  attackSpeedMult: number,
  onFloorCleared: (floor: number, wasGuardian: boolean) => void,
  onDamageDealt: (dmg: number) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }>; modifiedDamage?: number },
  signedPacts: number[],
): CombatTickResult {
  return processCombatTick(
    get, set, rawMana, elements, maxMana, attackSpeedMult,
    onFloorCleared, onDamageDealt, signedPacts,
    { activeGolems: [] },
    golemApplyDamageToRoom,
    (dmg: number) => dmg, // applyEnemyDefenses (passthrough for tests)
  );
}

// ═══════════════════════════════════════════════════════════════════════════════
// COMBAT ACTIONS — processCombatTick
// ═══════════════════════════════════════════════════════════════════════════════

describe('processCombatTick', () => {
  beforeEach(resetStores);

  describe('basic mechanics', () => {
    it('should return default result when currentAction is not climb', () => {
      useCombatStore.setState({ currentAction: 'meditate' });
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      expect(result.currentFloor).toBe(1);
      expect(result.floorHP).toBe(getFloorMaxHP(1));
    });

    it('should return default result when spell is not found', () => {
      useCombatStore.setState({ activeSpell: 'nonexistentSpell' });
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      expect(result.currentFloor).toBe(1);
    });

    it('should progress cast bar on each tick', () => {
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      expect(result.castProgress).toBeGreaterThan(0);
    });

    it('should reduce floor HP when cast completes', () => {
      // Set cast progress close to 1 so next cast completes
      useCombatStore.setState({ castProgress: 0.99 });
      const elements = makeInitialElements(500, {});
      const initialHP = useCombatStore.getState().floorHP;
      const result = runCombatTick(1000, elements);
      // Either HP decreased or floor advanced (if HP reached 0)
      expect(result.floorHP).toBeLessThanOrEqual(initialHP);
    });
  });

  describe('floor advancement', () => {
    it('should advance room when HP reaches 0 (not last room)', () => {
      // Set floor HP very low so it's cleared in one cast
      // currentRoomIndex=0, roomsPerFloor=5 → clears room 0, advances to room 1
      useCombatStore.setState({ castProgress: 0.99, climbDirection: 'up', currentRoomIndex: 0, roomsPerFloor: 5, currentRoom: { roomType: 'combat', enemies: [{ id: 'enemy', name: 'Test Enemy', hp: 1, maxHP: 100, armor: 0, dodgeChance: 0, barrier: 0, element: 'fire', activeEffects: [], effectiveArmor: 0 }] } });
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      // Room advanced, floor stays the same
      expect(result.currentFloor).toBe(1);
      // floorHP should be the new room's enemy HP
      expect(result.floorHP).toBeGreaterThan(0);
    });

    it('should advance floor when last room on floor is cleared', () => {
      // Set currentRoomIndex to last room so clearing it advances the floor
      useCombatStore.setState({ castProgress: 0.99, climbDirection: 'up', currentRoomIndex: 4, roomsPerFloor: 5, currentRoom: { roomType: 'combat', enemies: [{ id: 'enemy', name: 'Test Enemy', hp: 1, maxHP: 100, armor: 0, dodgeChance: 0, barrier: 0, element: 'fire', activeEffects: [], effectiveArmor: 0 }] } });
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      expect(result.currentFloor).toBe(2);
      expect(result.floorHP).toBeGreaterThan(0);
    });

    it('should update maxFloorReached when advancing', () => {
      useCombatStore.setState({ floorHP: 1, castProgress: 0.99, maxFloorReached: 5 });
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      expect(result.maxFloorReached).toBeGreaterThanOrEqual(5);
    });

    it('should not advance beyond floor 100', () => {
      useCombatStore.setState({ currentFloor: 100, floorHP: 1, castProgress: 0.99 });
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      expect(result.currentFloor).toBeLessThanOrEqual(100);
    });
  });

  describe('mana cost', () => {
    it('should deduct raw mana when spell costs raw mana', () => {
      const elements = makeInitialElements(500, {});
      const startMana = 1000;
      const result = runCombatTick(startMana, elements);
      // manaBolt costs raw mana, so after casting, rawMana should decrease
      expect(result.rawMana).toBeLessThanOrEqual(startMana);
    });

    it('should not cast when insufficient mana', () => {
      const elements = makeInitialElements(500, {});
      const state = useCombatStore.getState();
      // With 0 mana and 0 progress, no cast should complete
      const result = processCombatTickDirect(
        () => state,
        () => {},
        0, // no mana
        elements,
        1000,
        1,
        vi.fn(),
        (dmg) => ({ rawMana: 0, elements, modifiedDamage: dmg }),
        [],
      );
      expect(result.rawMana).toBe(0);
    });
  });

  describe('equipment spell states', () => {
    it('should initialize empty equipmentSpellStates', () => {
      const elements = makeInitialElements(500, {});
      const result = runCombatTick(1000, elements);
      expect(result.equipmentSpellStates).toBeDefined();
    });

    it('should not error with empty equipment spell states', () => {
      useCombatStore.setState({ equipmentSpellStates: [] });
      const elements = makeInitialElements(500, {});
      expect(() => runCombatTick(1000, elements)).not.toThrow();
    });
  });

  describe('error handling', () => {
    it('should return safe defaults when spell def is missing (early return path)', () => {
      // When the spell definition is not found, processCombatTick returns
      // a default result without crashing — this exercises the early return
      // path that acts as a safety net.
      useCombatStore.setState({ activeSpell: 'totallyInvalidSpell' });
      const elements = makeInitialElements(500, {});
      const result = processCombatTickDirect(
        () => useCombatStore.getState(),
        () => {},
        1000,
        elements,
        1000,
        1,
        vi.fn(),
        (dmg) => ({ rawMana: 1000, elements, modifiedDamage: dmg }),
        [],
      );
      expect(result).toBeDefined();
      expect(result.rawMana).toBe(1000);
      expect(result.logMessages).toBeDefined();
    });

    it('should not throw when onDamageDealt callback throws', () => {
      // The try/catch in processCombatTick should catch errors from callbacks
      // and return safe defaults instead of crashing
      const elements = makeInitialElements(500, {});
      useCombatStore.setState({ castProgress: 0.99 });
      expect(() => {
        processCombatTickDirect(
          () => useCombatStore.getState(),
          () => {},
          1000,
          elements,
          1000,
          1,
          vi.fn(),
          (_dmg) => { throw new Error('damage callback error'); },
          [],
        );
      }).not.toThrow();
    });
  });

  describe('helper: makeInitialSpells', () => {
    it('should create manaBolt as default spell', () => {
      const spells = makeInitialSpells();
      expect(spells.manaBolt).toBeDefined();
      expect(spells.manaBolt.learned).toBe(true);
      expect(spells.manaBolt.level).toBe(1);
    });

    it('should keep additional spells when specified', () => {
      const spells = makeInitialSpells(['fireball']);
      expect(spells.manaBolt).toBeDefined();
      expect(spells.fireball).toBeDefined();
      expect(spells.fireball.learned).toBe(true);
    });

    it('should not duplicate manaBolt when included in keep list', () => {
      const spells = makeInitialSpells(['manaBolt']);
      expect(Object.keys(spells).length).toBe(1);
    });
  });
});
