import { describe, it, expect } from 'vitest';
import {
  calculateStatBonus,
  calculateManaDrain,
  calculatePerkTier,
  canActivateDiscipline,
  canProceedDiscipline,
  getUnlockedPerks,
  calculateDisciplineStats,
} from '../utils/discipline-math';
import { DisciplinesAttunementType } from '../types/disciplines';
import type { DisciplineDefinition, DisciplineState } from '../types/disciplines';

// ─── Test Fixtures ────────────────────────────────────────────────────────────

const rawMastery: DisciplineDefinition = {
  id: 'raw-mastery',
  name: 'Raw Mana Mastery',
  attunement: DisciplinesAttunementType.BASE,
  manaType: 'raw',
  baseCost: 5,
  description: 'Learn to harness raw mana more efficiently.',
  statBonus: { stat: 'maxManaBonus', baseValue: 10, label: 'Max Mana Bonus' },
  difficultyFactor: 100,
  scalingFactor: 50,
  drainBase: 1,
  perks: [
    {
      id: 'raw-mastery-1',
      type: 'once',
      threshold: 100,
      value: 0,
      description: '+50 Max Mana',
    },
    {
      id: 'raw-mastery-2',
      type: 'infinite',
      threshold: 500,
      value: 100,
      description: 'Every 100 XP: +25 Max Mana',
    },
  ],
};

const attuneFire: DisciplineDefinition = {
  id: 'attune-fire',
  name: 'Fire Attunement',
  attunement: DisciplinesAttunementType.BASE,
  manaType: 'fire',
  baseCost: 10,
  description: 'Begin focusing raw mana into fire.',
  statBonus: { stat: 'elementCap_fire', baseValue: 5, label: 'Fire Element Cap' },
  difficultyFactor: 150,
  scalingFactor: 75,
  drainBase: 2,
  perks: [
    {
      id: 'elem-attunement-1',
      type: 'once',
      threshold: 200,
      value: 0,
      description: '+10 Fire Capacity',
    },
  ],
};

const cappedPerkDiscipline: DisciplineDefinition = {
  id: 'capped-test',
  name: 'Capped Perk Test',
  attunement: DisciplinesAttunementType.BASE,
  manaType: 'raw',
  baseCost: 1,
  description: 'Test discipline with capped perk.',
  statBonus: { stat: 'testStat', baseValue: 1, label: 'Test Stat' },
  difficultyFactor: 100,
  scalingFactor: 100,
  drainBase: 1,
  perks: [
    {
      id: 'capped-1',
      type: 'capped',
      threshold: 100,
      value: 50,
      description: 'Every 50 XP after 100: +1 tier',
    },
  ],
};

// ─── calculateStatBonus ───────────────────────────────────────────────────────

describe('calculateStatBonus', () => {
  it('should return 0 when xp is 0', () => {
    expect(calculateStatBonus(10, 0, 50)).toBe(0);
  });

  it('should return 0 when xp is negative', () => {
    expect(calculateStatBonus(10, -10, 50)).toBe(0);
  });

  it('should return baseValue when xp equals scalingFactor', () => {
    // ratio = 50/50 = 1, 1^0.65 = 1, bonus = 10 * 1 = 10
    const result = calculateStatBonus(10, 50, 50);
    expect(result).toBeCloseTo(10, 5);
  });

  it('should scale sub-linearly with xp', () => {
    const low = calculateStatBonus(10, 50, 50);
    const high = calculateStatBonus(10, 200, 50);
    // 4x xp should yield less than 4x bonus due to power 0.65
    expect(high).toBeGreaterThan(low);
    expect(high).toBeLessThan(low * 4);
  });

  it('should handle large xp values', () => {
    const result = calculateStatBonus(10, 10000, 50);
    expect(result).toBeGreaterThan(0);
    expect(isFinite(result)).toBe(true);
  });

  it('should handle very small scaling factor', () => {
    const result = calculateStatBonus(10, 10, 1);
    expect(result).toBeGreaterThan(0);
    expect(isFinite(result)).toBe(true);
  });
});

// ─── calculateManaDrain ───────────────────────────────────────────────────────

describe('calculateManaDrain', () => {
  it('should return baseDrain when xp is 0', () => {
    expect(calculateManaDrain(1, 0, 100)).toBe(1);
  });

  it('should return baseDrain when xp is negative', () => {
    expect(calculateManaDrain(1, -10, 100)).toBe(1);
  });

  it('should increase drain with xp', () => {
    const base = calculateManaDrain(1, 0, 100);
    const withXp = calculateManaDrain(1, 100, 100);
    expect(withXp).toBeGreaterThan(base);
  });

  it('should scale sub-linearly (power 0.4)', () => {
    const low = calculateManaDrain(1, 100, 100);
    const high = calculateManaDrain(1, 10000, 100);
    // 100x xp should yield less than 100x drain increase
    expect(high).toBeGreaterThan(low);
    expect(high - 1).toBeLessThan((low - 1) * 100);
  });

  it('should handle large xp values', () => {
    const result = calculateManaDrain(1, 100000, 100);
    expect(result).toBeGreaterThan(1);
    expect(isFinite(result)).toBe(true);
  });
});

// ─── calculatePerkTier ────────────────────────────────────────────────────────

describe('calculatePerkTier', () => {
  it('should return 0 when xp is below threshold', () => {
    expect(calculatePerkTier(50, 100, 50)).toBe(0);
  });

  it('should return 0 when xp equals threshold', () => {
    // excess = 0, floor(0/50) + 1 = 1, but xp < threshold is false
    // Actually: xp=100, threshold=100, excess=0, floor(0/50)+1 = 1
    expect(calculatePerkTier(100, 100, 50)).toBe(1);
  });

  it('should return 1 at threshold', () => {
    expect(calculatePerkTier(100, 100, 50)).toBe(1);
  });

  it('should return 2 after one interval past threshold', () => {
    // xp=150, threshold=100, excess=50, floor(50/50)+1 = 2
    expect(calculatePerkTier(150, 100, 50)).toBe(2);
  });

  it('should return 0 for xp just below threshold', () => {
    expect(calculatePerkTier(99, 100, 50)).toBe(0);
  });

  it('should handle zero xp', () => {
    expect(calculatePerkTier(0, 100, 50)).toBe(0);
  });
});

// ─── canActivateDiscipline ────────────────────────────────────────────────────

describe('canActivateDiscipline', () => {
  it('should return true for raw mana discipline regardless of elements', () => {
    const result = canActivateDiscipline(rawMastery, { elements: {} });
    expect(result).toBe(true);
  });

  it('should return true when required element is unlocked', () => {
    const result = canActivateDiscipline(attuneFire, {
      elements: { fire: { unlocked: true } },
    });
    expect(result).toBe(true);
  });

  it('should return false when required element is locked', () => {
    const result = canActivateDiscipline(attuneFire, {
      elements: { fire: { unlocked: false } },
    });
    expect(result).toBe(false);
  });

  it('should return falsy when required element does not exist', () => {
    const result = canActivateDiscipline(attuneFire, {
      elements: {},
    });
    expect(result).toBeFalsy();
  });

  it('should return falsy when elements is undefined', () => {
    const result = canActivateDiscipline(attuneFire, {});
    expect(result).toBeFalsy();
  });
});

// ─── canProceedDiscipline ─────────────────────────────────────────────────────

describe('canProceedDiscipline', () => {
  it('should return true when no state provided (optimistic)', () => {
    const result = canProceedDiscipline(rawMastery, undefined, undefined);
    expect(result).toBe(true);
  });

  it('should return true when discipline is paused (paused state is handled by activate, not canProceedDiscipline)', () => {
    const state: DisciplineState = { id: 'raw-mastery', xp: 0, paused: true };
    const result = canProceedDiscipline(rawMastery, state, {
      rawMana: 1000,
    });
    expect(result).toBe(true);
  });

  it('should return true when raw mana is sufficient', () => {
    const state: DisciplineState = { id: 'raw-mastery', xp: 0, paused: false };
    const result = canProceedDiscipline(rawMastery, state, {
      rawMana: 100,
    });
    expect(result).toBe(true);
  });

  it('should return true when raw mana is insufficient (no drain model)', () => {
    const state: DisciplineState = { id: 'raw-mastery', xp: 10000, paused: false };
    const result = canProceedDiscipline(rawMastery, state, {
      rawMana: 0,
    });
    expect(result).toBe(true);
  });

  it('should return true when element mana is sufficient', () => {
    const state: DisciplineState = { id: 'attune-fire', xp: 0, paused: false };
    const result = canProceedDiscipline(attuneFire, state, {
      elements: { fire: { current: 100, max: 100, unlocked: true } },
    });
    expect(result).toBe(true);
  });

  it('should return true when element exists (no drain model)', () => {
    const state: DisciplineState = { id: 'attune-fire', xp: 10000, paused: false };
    const result = canProceedDiscipline(attuneFire, state, {
      elements: { fire: { current: 0, max: 100, unlocked: true } },
    });
    expect(result).toBe(true);
  });

  it('should return false when element does not exist in game state', () => {
    const state: DisciplineState = { id: 'attune-fire', xp: 10000, paused: false };
    const result = canProceedDiscipline(attuneFire, state, {
      elements: {},
    });
    expect(result).toBe(false);
  });

  it('should return true when no game state provided (optimistic)', () => {
    const state: DisciplineState = { id: 'raw-mastery', xp: 100, paused: false };
    const result = canProceedDiscipline(rawMastery, state, undefined);
    expect(result).toBe(true);
  });
});

// ─── getUnlockedPerks ─────────────────────────────────────────────────────────

describe('getUnlockedPerks', () => {
  it('should return empty array when no perks are unlocked', () => {
    const result = getUnlockedPerks(rawMastery, 0);
    expect(result).toEqual([]);
  });

  it('should return once perk when threshold is met', () => {
    const result = getUnlockedPerks(rawMastery, 100);
    expect(result.length).toBe(1);
    expect(result[0].id).toBe('raw-mastery-1');
  });

  it('should return both once and infinite perks when both thresholds are met', () => {
    const result = getUnlockedPerks(rawMastery, 500);
    expect(result.length).toBe(2);
    expect(result.map(p => p.id)).toContain('raw-mastery-1');
    expect(result.map(p => p.id)).toContain('raw-mastery-2');
  });

  it('should return all perks at high xp', () => {
    const result = getUnlockedPerks(rawMastery, 10000);
    expect(result.length).toBe(2);
  });

  it('should return capped perk when threshold is met', () => {
    const result = getUnlockedPerks(cappedPerkDiscipline, 100);
    expect(result.length).toBe(1);
    expect(result[0].id).toBe('capped-1');
  });

  it('should return empty for xp just below threshold', () => {
    const result = getUnlockedPerks(rawMastery, 99);
    expect(result).toEqual([]);
  });
});

// ─── calculateDisciplineStats ─────────────────────────────────────────────────

describe('calculateDisciplineStats', () => {
  it('should return empty stats for empty arrays', () => {
    const result = calculateDisciplineStats([], []);
    expect(result).toEqual({});
  });

  it('should return empty stats when all disciplines are paused', () => {
    const states: DisciplineState[] = [
      { id: 'raw-mastery', xp: 1000, paused: true },
    ];
    const result = calculateDisciplineStats([rawMastery], states);
    expect(result).toEqual({});
  });

  it('should calculate stats for active discipline', () => {
    const states: DisciplineState[] = [
      { id: 'raw-mastery', xp: 50, paused: false },
    ];
    const result = calculateDisciplineStats([rawMastery], states);
    expect(result.maxManaBonus).toBeGreaterThan(0);
  });

  it('should sum stats from multiple active disciplines', () => {
    const disciplines = [rawMastery, attuneFire];
    const states: DisciplineState[] = [
      { id: 'raw-mastery', xp: 50, paused: false },
      { id: 'attune-fire', xp: 75, paused: false },
    ];
    const result = calculateDisciplineStats(disciplines, states);
    expect(result.maxManaBonus).toBeGreaterThan(0);
    expect(result.elementCap_fire).toBeGreaterThan(0);
  });

  it('should skip paused disciplines', () => {
    const disciplines = [rawMastery, attuneFire];
    const states: DisciplineState[] = [
      { id: 'raw-mastery', xp: 50, paused: false },
      { id: 'attune-fire', xp: 75, paused: true },
    ];
    const result = calculateDisciplineStats(disciplines, states);
    expect(result.maxManaBonus).toBeGreaterThan(0);
    expect(result.elementCap_fire).toBeUndefined();
  });

  it('should handle missing state for a discipline', () => {
    const disciplines = [rawMastery, attuneFire];
    const states: DisciplineState[] = [
      { id: 'raw-mastery', xp: 50, paused: false },
    ];
    const result = calculateDisciplineStats(disciplines, states);
    expect(result.maxManaBonus).toBeGreaterThan(0);
    expect(result.elementCap_fire).toBeUndefined();
  });

  it('should return zero stat bonus for zero xp', () => {
    const states: DisciplineState[] = [
      { id: 'raw-mastery', xp: 0, paused: false },
    ];
    const result = calculateDisciplineStats([rawMastery], states);
    expect(result.maxManaBonus).toBe(0);
  });
});
