import fc from 'fast-check';
import { describe, it } from 'vitest';
import { Character } from './characters/Character.ts';
import { Level } from './value-objects/Level.ts';
import { MagicalWeapon } from './magical-objects/MagicalWeapon.ts';
import { HealingObject } from './magical-objects/HealingObject.ts';

describe('Magical Objects', () => {
  describe('WeaponDealsDamage', () => {
    it('property: weapon deals its fixed damage amount', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 10 }),
          (damage, weaponHP, level) => {
            const attacker = Character.create({ name: 'hero', level: Level.create(level) });
            const target = Character.create({ name: 'goblin', level: Level.create(level) });
            const weapon = MagicalWeapon.create({
              damage,
              maxHealth: weaponHP,
              owner: attacker,
            });
            const result = attacker.useWeapon(weapon, target);
            return result.target.health.value === Math.max(0, 1000 - damage);
          },
        ),
      );
    });

    it('property: weapon health decreases by 1 after use', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 2, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          (weaponHP, damage) => {
            const attacker = Character.create({ name: 'hero', level: Level.create(1) });
            const target = Character.create({ name: 'goblin', level: Level.create(1) });
            const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
            const result = attacker.useWeapon(weapon, target);
            return result.weapon.health === weaponHP - 1;
          },
        ),
      );
    });

    it('property: weapon is destroyed when health reaches 0', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), (damage) => {
          const attacker = Character.create({ name: 'hero', level: Level.create(1) });
          const target = Character.create({ name: 'goblin', level: Level.create(1) });
          const weapon = MagicalWeapon.create({ damage, maxHealth: 1, owner: attacker });
          const result = attacker.useWeapon(weapon, target);
          return result.weapon.status.kind === 'destroyed';
        }),
      );
    });

    it('property: weapon remains alive when health > 0 after use', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 2, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          (weaponHP, damage) => {
            const attacker = Character.create({ name: 'hero', level: Level.create(1) });
            const target = Character.create({ name: 'goblin', level: Level.create(1) });
            const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
            const result = attacker.useWeapon(weapon, target);
            return result.weapon.status.kind === 'alive';
          },
        ),
      );
    });
  });

  describe('DeadCannotUseWeapon', () => {
    it('property: dead character cannot use weapon — state unchanged', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          (damage, weaponHP) => {
            const attacker = Character.create({ name: 'hero', level: Level.create(1) });
            const target = Character.create({ name: 'goblin', level: Level.create(1) });
            const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
            // Kill the attacker using a separate killer
            const killer = Character.create({ name: 'boss', level: Level.create(1) });
            const deadAttacker = killer.dealDamage(attacker, 10000);
            const weaponHPBefore = weapon.health;
            const targetHealthBefore = target.health.value;
            const result = deadAttacker.useWeapon(weapon, target);
            return (
              result.weapon.health === weaponHPBefore &&
              result.target.health.value === targetHealthBefore
            );
          },
        ),
      );
    });
  });

  describe('NonOwnerCannotUseWeapon', () => {
    it('property: non-owner cannot use weapon — state unchanged', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          (damage, weaponHP) => {
            const owner = Character.create({ name: 'owner', level: Level.create(1) });
            const thief = Character.create({ name: 'thief', level: Level.create(1) });
            const target = Character.create({ name: 'goblin', level: Level.create(1) });
            const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner });
            const weaponHPBefore = weapon.health;
            const targetHealthBefore = target.health.value;
            const result = thief.useWeapon(weapon, target);
            return (
              result.weapon.health === weaponHPBefore &&
              result.target.health.value === targetHealthBefore
            );
          },
        ),
      );
    });
  });

  describe('DestroyedWeaponCannotDealDamage', () => {
    it('property: destroyed weapon cannot deal damage — state unchanged', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), (damage) => {
          const owner = Character.create({ name: 'owner', level: Level.create(1) });
          const target = Character.create({ name: 'goblin', level: Level.create(1) });
          const weapon = MagicalWeapon.create({ damage, maxHealth: 1, owner });
          // Destroy the weapon first
          const firstUse = owner.useWeapon(weapon, target);
          const destroyedWeapon = firstUse.weapon;
          const targetHealthBefore = firstUse.target.health.value;
          // Try to use again on the destroyed weapon
          const result = owner.useWeapon(destroyedWeapon, firstUse.target);
          return result.weapon.health === 0 && result.target.health.value === targetHealthBefore;
        }),
      );
    });
  });

  describe('HealingObjectHealsCharacter', () => {
    it('property: healing object gives health up to its remaining health', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          (objectHP, healAmount, characterHealth) => {
            fc.pre(healAmount >= objectHP);
            fc.pre(characterHealth + objectHP <= 1000);
            const character = Character.createWithHealth({
              name: 'hero',
              level: Level.create(1),
              health: characterHealth,
            });
            const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
            const result = character.useHealingObject(object, healAmount);
            return result.character.health.value === characterHealth + objectHP;
          },
        ),
      );
    });

    it('property: healing object gives health up to character max when object has more', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 900, max: 999 }),
          (objectHP, healAmount, characterHealth) => {
            fc.pre(objectHP >= healAmount);
            fc.pre(characterHealth + healAmount > 1000);
            const character = Character.createWithHealth({
              name: 'hero',
              level: Level.create(1),
              health: characterHealth,
            });
            const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
            const result = character.useHealingObject(object, healAmount);
            return result.character.health.value === 1000;
          },
        ),
      );
    });

    it('property: healing object health decreases by actual healed amount', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 999 }),
          (objectHP, healAmount, characterHealth) => {
            fc.pre(characterHealth + healAmount <= 1000);
            fc.pre(healAmount <= objectHP);
            const character = Character.createWithHealth({
              name: 'hero',
              level: Level.create(1),
              health: characterHealth,
            });
            const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
            const result = character.useHealingObject(object, healAmount);
            return result.object.health === objectHP - healAmount;
          },
        ),
      );
    });

    it('property: healing object is destroyed when health reaches 0', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), (objectHP) => {
          // Character needs enough headroom to accept all object health
          const characterHealth = Math.max(1, 1000 - objectHP);
          const character = Character.createWithHealth({
            name: 'hero',
            level: Level.create(1),
            health: characterHealth,
          });
          const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
          const result = character.useHealingObject(object, objectHP);
          return result.object.status.kind === 'destroyed';
        }),
      );
    });

    it('property: healing object remains alive when health > 0 after use', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 2, max: 500 }),
          fc.integer({ min: 1, max: 999 }),
          (objectHP, characterHealth) => {
            const character = Character.createWithHealth({
              name: 'hero',
              level: Level.create(1),
              health: characterHealth,
            });
            const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
            const result = character.useHealingObject(object, 1);
            return result.object.status.kind === 'alive';
          },
        ),
      );
    });
  });

  describe('DeadCannotUseHealingObject', () => {
    it('property: dead character cannot use healing object — state unchanged', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), (objectHP) => {
          const character = Character.create({ name: 'hero', level: Level.create(1) });
          const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
          // Kill the character using a separate killer
          const killer = Character.create({ name: 'boss', level: Level.create(1) });
          const deadCharacter = killer.dealDamage(character, 10000);
          const objectHPBefore = object.health;
          const characterHealthBefore = deadCharacter.health.value;
          const result = deadCharacter.useHealingObject(object, 100);
          return (
            result.object.health === objectHPBefore &&
            result.character.health.value === characterHealthBefore
          );
        }),
      );
    });
  });

  describe('DestroyedHealingObjectCannotHeal', () => {
    it('property: destroyed healing object cannot heal — state unchanged', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), (objectHP) => {
          // Character needs headroom to drain the object
          const characterHealth = Math.max(1, 1000 - objectHP);
          const character = Character.createWithHealth({
            name: 'hero',
            level: Level.create(1),
            health: characterHealth,
          });
          const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
          // Drain the object to 0
          const firstUse = character.useHealingObject(object, objectHP);
          const destroyedObject = firstUse.object;
          const healedCharacter = firstUse.character;
          const characterHealthBefore = healedCharacter.health.value;
          // Try to use again on the destroyed object
          const result = healedCharacter.useHealingObject(destroyedObject, 100);
          return (
            result.object.health === 0 && result.character.health.value === characterHealthBefore
          );
        }),
      );
    });
  });

  describe('Invariants', () => {
    it('property: weapon health never goes negative', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 10000 }),
          (weaponHP, damage) => {
            const attacker = Character.create({ name: 'hero', level: Level.create(1) });
            const target = Character.create({ name: 'goblin', level: Level.create(1) });
            const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
            const result = attacker.useWeapon(weapon, target);
            return result.weapon.health >= 0;
          },
        ),
      );
    });

    it('property: healing object health never goes negative', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 10000 }),
          (objectHP, healAmount) => {
            const character = Character.create({ name: 'hero', level: Level.create(1) });
            const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
            const result = character.useHealingObject(object, healAmount);
            return result.object.health >= 0;
          },
        ),
      );
    });

    it('property: weapon health never exceeds maxHealth', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 500 }),
          (weaponHP, damage) => {
            const attacker = Character.create({ name: 'hero', level: Level.create(1) });
            const target = Character.create({ name: 'goblin', level: Level.create(1) });
            const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
            const result = attacker.useWeapon(weapon, target);
            return result.weapon.health <= weaponHP;
          },
        ),
      );
    });

    it('property: healing object health never exceeds maxHealth', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 1, max: 500 }),
          fc.integer({ min: 1, max: 10000 }),
          (objectHP, healAmount) => {
            const character = Character.create({ name: 'hero', level: Level.create(1) });
            const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
            const result = character.useHealingObject(object, healAmount);
            return result.object.health <= objectHP;
          },
        ),
      );
    });
  });
});
