import { describe, it, expect, vi, afterEach } from 'vitest';
import { Player } from '../../src/models/Player.js';
import { Person } from '../../src/models/Person.js';
import {
  computeTier,
  isFriendNpc,
  processWeeklyFriendshipTick,
} from '../../src/services/relationships/friendship_manager.js';

function makePlayer(): Player {
  const character = new Person({ firstname: 'Hero', sex: 'female' } as any);
  character.ageDays = 7300; // ~20 years
  character.ageYears = 20;
  character.happiness = 50;
  character.stress = 50;
  character.money = 100;
  const player = new Player({ userId: 'fr-test', character });
  player.r = [];
  return player;
}

function addFriend(player: Player, affinity: number, tier?: Person['friendshipTier']): Person {
  const friend = new Person({ firstname: 'Ana', sex: 'female' } as any);
  friend.id = `friend-${player.r.length}`;
  friend.status = 'alive';
  friend.relationships = ['friend'];
  friend.affinity = affinity;
  friend.friendshipTier = tier;
  friend.lastFriendInteractionDay = player.c.ageDays; // recently active by default
  player.r.push(friend);
  return friend;
}

afterEach(() => vi.restoreAllMocks());

describe('computeTier', () => {
  it('promotes at 30/60/85 and holds with hysteresis until 20/50/75', () => {
    expect(computeTier(10, undefined, false)).toBe('acquaintance');
    expect(computeTier(30, undefined, false)).toBe('friend');
    expect(computeTier(60, undefined, false)).toBe('close');
    expect(computeTier(85, undefined, false)).toBe('best');
    // hysteresis: a close friend at 55 stays close (demote only below 50)
    expect(computeTier(55, 'close', false)).toBe('close');
    expect(computeTier(49, 'close', false)).toBe('friend');
    // best at 80 stays best; below 75 drops
    expect(computeTier(80, 'best', false)).toBe('best');
    expect(computeTier(74, 'best', false)).toBe('close');
  });

  it('caps best at one: 85+ affinity stays close when a best slot is taken', () => {
    expect(computeTier(90, 'close', true)).toBe('close');
  });
});

describe('isFriendNpc', () => {
  it('matches friend tags, not family', () => {
    const p = new Person({ firstname: 'X', sex: 'male' } as any);
    p.status = 'alive';
    p.relationships = ['friend'];
    expect(isFriendNpc(p)).toBe(true);
    p.relationships = ['bestfriend'];
    expect(isFriendNpc(p)).toBe(true);
    p.relationships = ['sibling'];
    expect(isFriendNpc(p)).toBe(false);
  });
});

describe('processWeeklyFriendshipTick — tiers', () => {
  it('silently normalizes undefined tiers on old saves (no messages)', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99); // suppress beats
    const player = makePlayer();
    const f = addFriend(player, 70); // tier undefined
    f.lastFriendInteractionDay = undefined; // old save
    const events = processWeeklyFriendshipTick(player);
    expect(f.friendshipTier).toBe('close');
    expect(f.lastFriendInteractionDay).toBe(player.c.ageDays); // initialized
    expect(events).toHaveLength(0);
  });

  it('fires a promotion message when affinity crosses a tier up', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    const f = addFriend(player, 65, 'friend');
    const events = processWeeklyFriendshipTick(player);
    expect(f.friendshipTier).toBe('close');
    expect(events.some((e) => 'message' in e && e.message.includes('Ana'))).toBe(true);
  });

  it('only one best friend at a time', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    addFriend(player, 90, 'best');
    const second = addFriend(player, 95, 'close');
    processWeeklyFriendshipTick(player);
    expect(second.friendshipTier).toBe('close');
  });
});

describe('processWeeklyFriendshipTick — neglect, fade, payoffs', () => {
  it('drifts affinity -2/week after 28 days without interaction', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    const f = addFriend(player, 40, 'friend');
    f.lastFriendInteractionDay = player.c.ageDays - 30;
    processWeeklyFriendshipTick(player);
    expect(f.affinity).toBe(38);
  });

  it('does NOT drift recently-active friendships', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    const f = addFriend(player, 40, 'friend');
    f.lastFriendInteractionDay = player.c.ageDays - 5;
    processWeeklyFriendshipTick(player);
    expect(f.affinity).toBe(40);
  });

  it('fades a neglected dead friendship: tags removed + message', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    const f = addFriend(player, 0, 'acquaintance');
    f.lastFriendInteractionDay = player.c.ageDays - 100;
    const events = processWeeklyFriendshipTick(player);
    expect(f.relationships).not.toContain('friend');
    expect(f.friendshipTier).toBeUndefined();
    expect(events.some((e) => 'message' in e && e.message.includes('faded'))).toBe(true);
  });

  it('close friends buffer happiness; a best friend eases stress', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    addFriend(player, 70, 'close');
    addFriend(player, 70, 'close');
    addFriend(player, 90, 'best');
    player.c.happiness = 50;
    player.c.stress = 50;
    processWeeklyFriendshipTick(player);
    expect(player.c.happiness).toBe(53); // 2 close (cap 2) + 1 best
    expect(player.c.stress).toBe(48);
  });

  it('best friend bails you out when broke, once per year', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    const best = addFriend(player, 90, 'best');
    player.c.money = -150;
    const events = processWeeklyFriendshipTick(player);
    expect(player.c.money).toBe(200); // -150 + min(600, 150+200)
    expect(player.lastFriendBailoutDay).toBe(Math.floor(player.c.ageDays));
    expect(events.some((e) => 'message' in e && e.message.includes('covered you'))).toBe(true);

    // second time within the year: no bailout
    player.c.money = -100;
    const again = processWeeklyFriendshipTick(player);
    expect(player.c.money).toBe(-100);
    expect(again.some((e) => 'message' in e && e.message.includes('covered you'))).toBe(false);
  });

  it('no bailout without a best friend', () => {
    vi.spyOn(Math, 'random').mockReturnValue(0.99);
    const player = makePlayer();
    addFriend(player, 70, 'close');
    player.c.money = -150;
    processWeeklyFriendshipTick(player);
    expect(player.c.money).toBe(-150);
  });

  it('weekly beat stamps lastFriendInteractionDay on positive beats', () => {
    const player = makePlayer();
    const f = addFriend(player, 70, 'close');
    f.lastFriendInteractionDay = player.c.ageDays - 10;
    // random: 0.0 -> beat fires, picks first friend, first CLOSE beat (positive)
    vi.spyOn(Math, 'random').mockReturnValue(0.0);
    const events = processWeeklyFriendshipTick(player);
    expect(events.some((e) => 'message' in e)).toBe(true);
    expect(f.lastFriendInteractionDay).toBe(Math.floor(player.c.ageDays));
    expect(f.affinity).toBeGreaterThan(70);
  });
});
