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

describe('Factions', () => {
  const hero = () => Character.create({ name: 'hero', level: Level.create(1) });
  const ally = () => Character.create({ name: 'ally', level: Level.create(1) });
  const enemy = () => Character.create({ name: 'enemy', level: Level.create(1) });

  describe('JoinFaction', () => {
    it("property: joining a faction adds it to the character's faction set", () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const c = hero();
            const f = Faction.create(factionName);
            const result = c.joinFaction(f);
            return result.factions.has(f) && result.factions.size === 1;
          },
        ),
      );
    });

    it('property: joining a second faction adds it without removing the first', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (name1, name2) => {
            fc.pre(name1 !== name2);
            const c = hero();
            const f1 = Faction.create(name1);
            const f2 = Faction.create(name2);
            const withFirst = c.joinFaction(f1);
            const withBoth = withFirst.joinFaction(f2);
            return (
              withBoth.factions.has(f1) && withBoth.factions.has(f2) && withBoth.factions.size === 2
            );
          },
        ),
      );
    });

    it('property: joining the same faction twice is idempotent', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const c = hero();
            const f = Faction.create(factionName);
            const once = c.joinFaction(f);
            const twice = once.joinFaction(f);
            return once.factions.size === twice.factions.size && twice.factions.has(f);
          },
        ),
      );
    });

    it('property: joining a faction returns a new Character (not the same reference)', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const c = hero();
            const f = Faction.create(factionName);
            const result = c.joinFaction(f);
            return result !== c;
          },
        ),
      );
    });

    it('property: joining a faction preserves health, status, and level', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 0, max: 1000 }),
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (health, factionName) => {
            const c = Character.createWithHealth({ name: 'hero', level: Level.create(1), health });
            const f = Faction.create(factionName);
            const result = c.joinFaction(f);
            return (
              result.health.value === health &&
              result.status.kind === 'alive' &&
              result.level.value === 1
            );
          },
        ),
      );
    });
  });

  describe('LeaveFaction', () => {
    it("property: leaving a faction removes it from the character's faction set", () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const c = hero();
            const f = Faction.create(factionName);
            const withFaction = c.joinFaction(f);
            const result = withFaction.leaveFaction(f);
            return !result.factions.has(f) && result.factions.size === 0;
          },
        ),
      );
    });

    it('property: leaving one faction preserves the others', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 2, maxLength: 20 }).filter((s) => s.trim().length > 0),
          fc.string({ minLength: 2, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (keepName, leaveName) => {
            fc.pre(keepName !== leaveName);
            const c = hero();
            const fKeep = Faction.create(keepName);
            const fLeave = Faction.create(leaveName);
            const withBoth = c.joinFaction(fKeep).joinFaction(fLeave);
            const result = withBoth.leaveFaction(fLeave);
            return (
              result.factions.has(fKeep) &&
              !result.factions.has(fLeave) &&
              result.factions.size === 1
            );
          },
        ),
      );
    });

    it('property: leaving a faction you do not belong to is a no-op', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const c = hero();
            const f = Faction.create(factionName);
            const result = c.leaveFaction(f);
            return result === c && result.factions.size === 0;
          },
        ),
      );
    });

    it('property: leaving a faction returns a new Character (not the same reference)', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const c = hero();
            const f = Faction.create(factionName);
            const withFaction = c.joinFaction(f);
            const result = withFaction.leaveFaction(f);
            return result !== withFaction;
          },
        ),
      );
    });

    it('property: leaving a faction preserves health, status, and level', () => {
      fc.assert(
        fc.property(
          fc.integer({ min: 0, max: 1000 }),
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (health, factionName) => {
            const c = Character.createWithHealth({ name: 'hero', level: Level.create(1), health });
            const f = Faction.create(factionName);
            const withFaction = c.joinFaction(f);
            const result = withFaction.leaveFaction(f);
            return (
              result.health.value === health &&
              result.status.kind === 'alive' &&
              result.level.value === 1
            );
          },
        ),
      );
    });
  });

  describe('DeadCannotJoinFaction', () => {
    it('property: dead characters cannot join a faction — state unchanged', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const attacker = Character.create({ name: 'attacker', level: Level.create(1) });
            const hero = Character.create({ name: 'hero', level: Level.create(1) });
            const deadHero = attacker.dealDamage(hero, 10000);
            const f = Faction.create(factionName);
            const result = deadHero.joinFaction(f);
            return result === deadHero && result.factions.size === 0;
          },
        ),
      );
    });
  });

  describe('DeadCannotLeaveFaction', () => {
    it('property: dead characters cannot leave a faction — state unchanged', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          (factionName) => {
            const attacker = Character.create({ name: 'attacker', level: Level.create(1) });
            const hero = Character.create({ name: 'hero', level: Level.create(1) });
            const f = Faction.create(factionName);
            const withFaction = hero.joinFaction(f);
            const deadHero = attacker.dealDamage(withFaction, 10000);
            const result = deadHero.leaveFaction(f);
            return result === deadHero && result.factions.has(f);
          },
        ),
      );
    });
  });

  describe('AllyRelation', () => {
    it('property: two characters sharing a faction are allies', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          () => {
            const faction = Faction.create('knights');
            const c1 = hero().joinFaction(faction);
            const c2 = ally().joinFaction(faction);
            return c1.isAllyOf(c2) && c2.isAllyOf(c1);
          },
        ),
      );
    });

    it('property: two characters with no shared factions are not allies', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          () => {
            const c1 = hero().joinFaction(Faction.create('knights'));
            const c2 = ally().joinFaction(Faction.create('merchants'));
            return !c1.isAllyOf(c2) && !c2.isAllyOf(c1);
          },
        ),
      );
    });

    it('property: a character is not an ally of itself', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          () => {
            const c = hero().joinFaction(Faction.create('knights'));
            return !c.isAllyOf(c);
          },
        ),
      );
    });

    it('property: ally relation is symmetric', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          () => {
            const faction = Faction.create('guard');
            const c1 = hero().joinFaction(faction);
            const c2 = ally().joinFaction(faction);
            return c1.isAllyOf(c2) === c2.isAllyOf(c1);
          },
        ),
      );
    });

    it('property: sharing one of multiple factions makes allies', () => {
      fc.assert(
        fc.property(
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          fc.string({ minLength: 1, maxLength: 20 }).filter((s) => s.trim().length > 0),
          () => {
            const shared = Faction.create('shared');
            const only1 = Faction.create('only1');
            const only2 = Faction.create('only2');
            const c1 = hero().joinFaction(shared).joinFaction(only1);
            const c2 = ally().joinFaction(shared).joinFaction(only2);
            return c1.isAllyOf(c2) && c2.isAllyOf(c1);
          },
        ),
      );
    });
  });

  describe('AllyDamageForbidden', () => {
    it('property: allies cannot deal damage to each other — health unchanged', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 5000 }), () => {
          const faction = Faction.create('knights');
          const attacker = hero().joinFaction(faction);
          const target = ally().joinFaction(faction);
          const healthBefore = target.health.value;
          const statusBefore = target.status.kind;
          const result = attacker.dealDamage(target, 500);
          return result.health.value === healthBefore && result.status.kind === statusBefore;
        }),
      );
    });

    it('property: allies cannot deal damage to each other — returns original target reference', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 5000 }), () => {
          const faction = Faction.create('guard');
          const attacker = hero().joinFaction(faction);
          const target = ally().joinFaction(faction);
          const result = attacker.dealDamage(target, 500);
          return result === target;
        }),
      );
    });

    it('property: non-allies can deal damage normally', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), () => {
          const attacker = hero();
          const target = enemy();
          const healthBefore = target.health.value;
          const result = attacker.dealDamage(target, 100);
          return result.health.value === healthBefore - 100;
        }),
      );
    });
  });

  describe('AllyHealAllowed', () => {
    it('property: allies can heal each other', () => {
      fc.assert(
        fc.property(fc.integer({ min: 0, max: 900 }), fc.integer({ min: 1, max: 200 }), () => {
          const faction = Faction.create('healers');
          const healer = hero().joinFaction(faction);
          const ally = Character.createWithHealth({
            name: 'ally',
            level: Level.create(1),
            health: 100,
          }).joinFaction(faction);
          const healthBefore = ally.health.value;
          const result = healer.healAlly(ally, 50);
          return result.health.value === healthBefore + 50;
        }),
      );
    });

    it('property: ally healing is capped at max health for level', () => {
      fc.assert(
        fc.property(fc.integer({ min: 950, max: 1000 }), fc.integer({ min: 1, max: 200 }), () => {
          const faction = Faction.create('healers');
          const healer = hero().joinFaction(faction);
          const ally = Character.createWithHealth({
            name: 'ally',
            level: Level.create(1),
            health: 980,
          }).joinFaction(faction);
          const result = healer.healAlly(ally, 100);
          return result.health.value === 1000;
        }),
      );
    });

    it('property: ally healing returns a new Character (not the same reference)', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 200 }), () => {
          const faction = Faction.create('healers');
          const healer = hero().joinFaction(faction);
          const allyChar = ally().joinFaction(faction);
          const result = healer.healAlly(allyChar, 50);
          return result !== allyChar;
        }),
      );
    });
  });

  describe('NonAllyHealForbidden', () => {
    it('property: non-allies cannot heal each other — health unchanged', () => {
      fc.assert(
        fc.property(fc.integer({ min: 0, max: 1000 }), () => {
          const healer = hero();
          const target = enemy();
          const healthBefore = target.health.value;
          const result = healer.healAlly(target, 50);
          return result.health.value === healthBefore;
        }),
      );
    });

    it('property: non-allies healing returns the original target reference', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), () => {
          const healer = hero();
          const target = enemy();
          const result = healer.healAlly(target, 50);
          return result === target;
        }),
      );
    });
  });

  describe('DeadCannotBeHealedByAlly', () => {
    it('property: dead allies cannot be healed — state unchanged', () => {
      fc.assert(
        fc.property(fc.integer({ min: 1, max: 500 }), () => {
          const faction = Faction.create('guard');
          const healer = hero().joinFaction(faction);
          const target = ally().joinFaction(faction);
          // Kill the target with a non-ally attacker
          const killer = enemy();
          const deadTarget = killer.dealDamage(target, 10000);
          const healthBefore = deadTarget.health.value;
          const statusBefore = deadTarget.status.kind;
          const result = healer.healAlly(deadTarget, 500);
          return (
            result === deadTarget &&
            result.health.value === healthBefore &&
            result.status.kind === statusBefore
          );
        }),
      );
    });
  });
});
