import { describe, it, expect } from 'bun:test';
import { World } from '@common/rpg/core/world';
import { Factions } from '@common/rpg/components/faction';

function world() { return new World(); }

// ── Membership ────────────────────────────────────────────────────────────────

describe('Factions.join / isMember / leave', () => {
    it('isMember returns false for a brand-new entity', () => {
        const w = world();
        expect(Factions.isMember(w.createEntity(), 'guards')).toBeFalse();
    });

    it('join makes isMember return true', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        expect(Factions.isMember(e, 'guards')).toBeTrue();
    });

    it('isMember is faction-specific', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        expect(Factions.isMember(e, 'bandits')).toBeFalse();
    });

    it('join is idempotent — double-joining does not duplicate', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        Factions.join(e, 'guards');
        expect(Factions.getFactions(e).filter(f => f === 'guards').length).toBe(1);
    });

    it('entity can belong to multiple factions simultaneously', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        Factions.join(e, 'merchants');
        Factions.join(e, 'mages');
        expect(Factions.getFactions(e).sort()).toEqual(['guards', 'mages', 'merchants']);
    });

    it('leave removes membership', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        Factions.leave(e, 'guards');
        expect(Factions.isMember(e, 'guards')).toBeFalse();
    });

    it('leave is safe on a non-member', () => {
        const w = world();
        const e = w.createEntity();
        expect(() => Factions.leave(e, 'guards')).not.toThrow();
    });

    it('leave only removes the specified faction', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        Factions.join(e, 'merchants');
        Factions.leave(e, 'guards');
        expect(Factions.isMember(e, 'guards')).toBeFalse();
        expect(Factions.isMember(e, 'merchants')).toBeTrue();
    });

    it('can re-join after leaving', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        Factions.leave(e, 'guards');
        Factions.join(e, 'guards');
        expect(Factions.isMember(e, 'guards')).toBeTrue();
    });

    it('getFactions returns empty array for new entity', () => {
        const w = world();
        expect(Factions.getFactions(w.createEntity())).toEqual([]);
    });

    it('getFactions reflects leave', () => {
        const w = world();
        const e = w.createEntity();
        Factions.join(e, 'guards');
        Factions.join(e, 'merchants');
        Factions.leave(e, 'merchants');
        expect(Factions.getFactions(e)).toEqual(['guards']);
    });
});

// ── getFaction ─────────────────────────────────────────────────────────────────

describe('Factions.getFaction', () => {
    it('creates a faction entity on first call', () => {
        const w = world();
        const faction = Factions.getFaction(w, 'guards');
        expect(faction).toBeDefined();
    });

    it('returns the same component for the same factionId', () => {
        const w = world();
        const a = Factions.getFaction(w, 'guards');
        const b = Factions.getFaction(w, 'guards');
        expect(a).toBe(b);
    });

    it('returns different components for different factionIds', () => {
        const w = world();
        const guards = Factions.getFaction(w, 'guards');
        const bandits = Factions.getFaction(w, 'bandits');
        expect(guards).not.toBe(bandits);
    });
});

// ── getReputationOf / setReputationOf / adjustReputationOf ────────────────────
// Observer's personal opinion of a named faction.

describe('Factions.setReputationOf / getReputationOf', () => {
    it('getReputationOf returns 0 with no data', () => {
        const w = world();
        expect(Factions.getReputationOf(w.createEntity(), 'guards')).toBe(0);
    });

    it('setReputationOf persists the value', () => {
        const w = world();
        const e = w.createEntity();
        Factions.setReputationOf(e, 'guards', 50);
        expect(Factions.getReputationOf(e, 'guards')).toBe(50);
    });

    it('setReputationOf overwrites the previous value without duplication', () => {
        const w = world();
        const e = w.createEntity();
        Factions.setReputationOf(e, 'guards', 50);
        Factions.setReputationOf(e, 'guards', 75);
        expect(Factions.getReputationOf(e, 'guards')).toBe(75);
    });

    it('setReputationOf allows negative values', () => {
        const w = world();
        const e = w.createEntity();
        Factions.setReputationOf(e, 'bandits', -100);
        expect(Factions.getReputationOf(e, 'bandits')).toBe(-100);
    });

    it('reputations are independent per faction', () => {
        const w = world();
        const e = w.createEntity();
        Factions.setReputationOf(e, 'guards', 80);
        Factions.setReputationOf(e, 'bandits', -20);
        expect(Factions.getReputationOf(e, 'guards')).toBe(80);
        expect(Factions.getReputationOf(e, 'bandits')).toBe(-20);
    });

    it('reputations are independent per observer', () => {
        const w = world();
        const a = w.createEntity();
        const b = w.createEntity();
        Factions.setReputationOf(a, 'guards', 60);
        Factions.setReputationOf(b, 'guards', 10);
        expect(Factions.getReputationOf(a, 'guards')).toBe(60);
        expect(Factions.getReputationOf(b, 'guards')).toBe(10);
    });
});

describe('Factions.adjustReputationOf', () => {
    it('adjusts from 0 by default', () => {
        const w = world();
        const e = w.createEntity();
        Factions.adjustReputationOf(e, 'guards', 20);
        expect(Factions.getReputationOf(e, 'guards')).toBe(20);
    });

    it('accumulates positive adjustments', () => {
        const w = world();
        const e = w.createEntity();
        Factions.adjustReputationOf(e, 'guards', 20);
        Factions.adjustReputationOf(e, 'guards', 15);
        expect(Factions.getReputationOf(e, 'guards')).toBe(35);
    });

    it('accumulates negative adjustments', () => {
        const w = world();
        const e = w.createEntity();
        Factions.adjustReputationOf(e, 'guards', 20);
        Factions.adjustReputationOf(e, 'guards', -5);
        expect(Factions.getReputationOf(e, 'guards')).toBe(15);
    });

    it('can drive reputation below zero', () => {
        const w = world();
        const e = w.createEntity();
        Factions.setReputationOf(e, 'guards', 5);
        Factions.adjustReputationOf(e, 'guards', -10);
        expect(Factions.getReputationOf(e, 'guards')).toBe(-5);
    });

    it('adjustments are faction-specific', () => {
        const w = world();
        const e = w.createEntity();
        Factions.adjustReputationOf(e, 'guards', 30);
        Factions.adjustReputationOf(e, 'bandits', -10);
        expect(Factions.getReputationOf(e, 'guards')).toBe(30);
        expect(Factions.getReputationOf(e, 'bandits')).toBe(-10);
    });
});

// Cross-faction reputation aggregation: observer inherits their own faction's opinion.

describe('Factions.getReputationOf — cross-faction aggregation', () => {
    it("observer inherits their faction's reputation toward another faction", () => {
        const w = world();
        const observer = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;

        Factions.join(observer, 'guards');
        Factions.setReputationOf(guardsFaction, 'bandits', -50);

        // observer personally has 0, but via guards membership inherits -50
        expect(Factions.getReputationOf(observer, 'bandits')).toBe(-50);
    });

    it('personal and inherited reputations are summed', () => {
        const w = world();
        const observer = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;

        Factions.join(observer, 'guards');
        Factions.setReputationOf(observer, 'bandits', 20);
        Factions.setReputationOf(guardsFaction, 'bandits', 30);

        expect(Factions.getReputationOf(observer, 'bandits')).toBe(50);
    });

    it('cross-faction contributions from multiple memberships are all summed', () => {
        const w = world();
        const observer = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;
        const merchantsFaction = Factions.getFaction(w, 'merchants').entity;

        Factions.join(observer, 'guards');
        Factions.join(observer, 'merchants');
        Factions.setReputationOf(guardsFaction, 'bandits', -40);
        Factions.setReputationOf(merchantsFaction, 'bandits', 10);

        expect(Factions.getReputationOf(observer, 'bandits')).toBe(-30);
    });

    it('skipAggregation=true returns only personal reputation', () => {
        const w = world();
        const observer = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;

        Factions.join(observer, 'guards');
        Factions.setReputationOf(observer, 'bandits', 10);
        Factions.setReputationOf(guardsFaction, 'bandits', 50);

        expect(Factions.getReputationOf(observer, 'bandits', true)).toBe(10);
    });
});

// ── getReputationIn / setReputationIn / adjustReputationIn ────────────────────
// A named faction's opinion of a target entity.

describe('Factions.setReputationIn / getReputationIn', () => {
    it('getReputationIn returns 0 with no data', () => {
        const w = world();
        expect(Factions.getReputationIn(w.createEntity(), 'guards')).toBe(0);
    });

    it('setReputationIn persists the value', () => {
        const w = world();
        const target = w.createEntity();
        Factions.setReputationIn(target, 'guards', 40);
        expect(Factions.getReputationIn(target, 'guards')).toBe(40);
    });

    it('setReputationIn overwrites previous value', () => {
        const w = world();
        const target = w.createEntity();
        Factions.setReputationIn(target, 'guards', 40);
        Factions.setReputationIn(target, 'guards', 80);
        expect(Factions.getReputationIn(target, 'guards')).toBe(80);
    });

    it('setReputationIn allows negative values', () => {
        const w = world();
        const target = w.createEntity();
        Factions.setReputationIn(target, 'bandits', -60);
        expect(Factions.getReputationIn(target, 'bandits')).toBe(-60);
    });

    it('opinions are independent per faction', () => {
        const w = world();
        const target = w.createEntity();
        Factions.setReputationIn(target, 'guards', 50);
        Factions.setReputationIn(target, 'bandits', -30);
        expect(Factions.getReputationIn(target, 'guards')).toBe(50);
        expect(Factions.getReputationIn(target, 'bandits')).toBe(-30);
    });

    it('opinions are independent per target', () => {
        const w = world();
        const t1 = w.createEntity();
        const t2 = w.createEntity();
        Factions.setReputationIn(t1, 'guards', 70);
        Factions.setReputationIn(t2, 'guards', 20);
        expect(Factions.getReputationIn(t1, 'guards')).toBe(70);
        expect(Factions.getReputationIn(t2, 'guards')).toBe(20);
    });
});

describe('Factions.adjustReputationIn', () => {
    it('adjusts from 0 by default', () => {
        const w = world();
        const target = w.createEntity();
        Factions.adjustReputationIn(target, 'guards', 25);
        expect(Factions.getReputationIn(target, 'guards')).toBe(25);
    });

    it('accumulates correctly', () => {
        const w = world();
        const target = w.createEntity();
        Factions.adjustReputationIn(target, 'guards', 25);
        Factions.adjustReputationIn(target, 'guards', -10);
        expect(Factions.getReputationIn(target, 'guards')).toBe(15);
    });
});

describe('Factions.getReputationIn — faction-entity target routing', () => {
    it("when target is a faction entity, returns the observing faction's opinion of that faction", () => {
        const w = world();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;
        const banditsFaction = Factions.getFaction(w, 'bandits').entity;

        Factions.setReputationOf(guardsFaction, 'bandits', -80);

        // getReputationIn(banditsFaction, 'guards') = "how do guards see the bandits faction?"
        // routes to getReputationOf(guardsFaction, 'bandits')
        expect(Factions.getReputationIn(banditsFaction, 'guards')).toBe(-80);
    });
});

describe('Factions.getReputationIn — cross-faction aggregation', () => {
    it("faction's opinion of a target aggregates across target's faction memberships", () => {
        const w = world();
        const target = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;

        Factions.join(target, 'merchants');
        Factions.setReputationOf(guardsFaction, 'merchants', 35);

        // guards look at target who is in merchants — cross-faction adds 35
        expect(Factions.getReputationIn(target, 'guards')).toBe(35);
    });

    it('personal and cross-faction contributions are summed', () => {
        const w = world();
        const target = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;

        Factions.join(target, 'merchants');
        Factions.setReputationIn(target, 'guards', 10);
        Factions.setReputationOf(guardsFaction, 'merchants', 25);

        expect(Factions.getReputationIn(target, 'guards')).toBe(35);
    });
});

// ── getReputation (full observer → target resolution) ─────────────────────────

describe('Factions.getReputation', () => {
    it('returns 0 for two entities with no factions or reputation', () => {
        const w = world();
        expect(Factions.getReputation(w.createEntity(), w.createEntity())).toBe(0);
    });

    it('when target is a faction entity, returns observer rep for that faction', () => {
        const w = world();
        const observer = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;

        Factions.setReputationOf(observer, 'guards', 55);
        expect(Factions.getReputation(observer, guardsFaction)).toBe(55);
    });

    it('sums observer rep across all factions target belongs to', () => {
        const w = world();
        const observer = w.createEntity();
        const target = w.createEntity();

        Factions.join(target, 'guards');
        Factions.join(target, 'merchants');
        Factions.setReputationOf(observer, 'guards', 30);
        Factions.setReputationOf(observer, 'merchants', 20);

        // target is in guards+merchants, observer's rep with each is summed
        expect(Factions.getReputation(observer, target)).toBeGreaterThanOrEqual(50);
    });

    it("observer's faction membership contributes toward target's faction opinion", () => {
        const w = world();
        const observer = w.createEntity();
        const target = w.createEntity();
        const guardsFaction = Factions.getFaction(w, 'guards').entity;

        Factions.join(observer, 'guards');
        Factions.join(target, 'bandits');
        Factions.setReputationOf(guardsFaction, 'bandits', -40);

        // guards hate bandits; observer (a guard) sees target (a bandit) more negatively
        expect(Factions.getReputation(observer, target)).toBeLessThan(0);
    });

    it('personal opinion is included alongside cross-faction aggregation', () => {
        const w = world();
        const observer = w.createEntity();
        const target = w.createEntity();

        Factions.join(target, 'guards');
        Factions.setReputationOf(observer, 'guards', 40);

        expect(Factions.getReputation(observer, target)).toBe(40);
    });

    it('symmetric scenario: both sides in factions with mutual opinions', () => {
        const w = world();
        const hero = w.createEntity();
        const villain = w.createEntity();
        const heroFaction = Factions.getFaction(w, 'heroes').entity;
        const villainFaction = Factions.getFaction(w, 'villains').entity;

        Factions.join(hero, 'heroes');
        Factions.join(villain, 'villains');

        // heroes hate villains
        Factions.setReputationOf(heroFaction, 'villains', -100);
        // villains hate heroes
        Factions.setReputationOf(villainFaction, 'heroes', -100);

        const heroSeesVillain = Factions.getReputation(hero, villain);
        const villainSeesHero = Factions.getReputation(villain, hero);

        expect(heroSeesVillain).toBeLessThan(0);
        expect(villainSeesHero).toBeLessThan(0);
    });

    it('neutral factions produce no reputation effect', () => {
        const w = world();
        const a = w.createEntity();
        const b = w.createEntity();

        Factions.join(a, 'merchants');
        Factions.join(b, 'farmers');
        // no reputation set between merchants and farmers

        expect(Factions.getReputation(a, b)).toBe(0);
    });

    it('target with no faction memberships yields only observer-set personal rep (0 if none)', () => {
        const w = world();
        const observer = w.createEntity();
        const target = w.createEntity();

        Factions.join(observer, 'guards');
        // target has no factions

        expect(Factions.getReputation(observer, target)).toBe(0);
    });
});
