import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Person } from '../../../src/models/Person.js';
import { Player } from '../../../src/models/Player.js';

const { checkAchievementsAsyncMock, trackMarriageMock, trackDatingMock, trackAffinityMock } =
  vi.hoisted(() => ({
    checkAchievementsAsyncMock: vi.fn(async () => null),
    trackMarriageMock: vi.fn(),
    trackDatingMock: vi.fn(),
    trackAffinityMock: vi.fn(),
  }));

vi.mock('../../../src/services/retention/index.js', () => ({
  checkAchievementsAsync: checkAchievementsAsyncMock,
  trackMarriage: trackMarriageMock,
  trackDating: trackDatingMock,
  trackAffinity: trackAffinityMock,
  getPlayerStatistics: vi.fn(() => ({})),
}));

import {
  breakUp,
  createRelationship,
  dateNight,
  divorce,
  establishMatchRelationship,
  getActiveRelationship,
  getRelData,
  marry,
  propose,
  romance,
} from '../../../src/services/relationships/relationship_manager.js';

function createPlayerWithPartner(affinity = 60): { player: Player; partner: Person } {
  const character = new Person({
    id: 'player-char',
    firstname: 'Alex',
    lastname: 'Player',
    sex: 'Male',
    ageYears: 24,
    energy: 100,
    money: 500,
    relationships: ['dating_match', 'partner'],
  });

  const partner = new Person({
    id: 'partner-1',
    firstname: 'Taylor',
    lastname: 'Partner',
    sex: 'Female',
    ageYears: 24,
    affinity,
    relationships: ['dating_match', 'partner'],
  });

  const player = new Player({
    userId: 'user-1',
    character,
    r: [partner],
    status: 'playing',
    date: '2026-02-07',
  });

  return { player, partner };
}

describe('relationship manager regressions', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('allows date nights for prospect relationships and transitions prospect to dating', () => {
    const { player, partner } = createPlayerWithPartner(65);
    const started = romance(player, partner.id);
    expect(started).toBe(true);

    const relBefore = getRelData(player, partner.id);
    expect(relBefore?.relationshipStatus).toBe('Prospect');

    vi.spyOn(Math, 'random').mockReturnValue(0);
    const result = dateNight(player, 'Movie Night at Home');

    expect(result).not.toBeNull();
    const relAfter = getRelData(player, partner.id);
    expect(relAfter?.relationshipStatus).toBe('Dating');
    expect(player.c.partner).toBe(partner.id);
    expect(partner.partner).toBe(player.c.id);
  });

  it('cleans up all relationship data on breakup', () => {
    const { player, partner } = createPlayerWithPartner();
    const relationship = createRelationship(
      player.c.id,
      partner.id,
      player.date,
      'Dating',
      'You are dating.'
    );

    player.relData.push(relationship as unknown as (typeof player.relData)[number]);
    (player.c as unknown as { relationship: string | null }).relationship = relationship.id;
    player.c.partner = partner.id;
    (partner as unknown as { relationship: string | null }).relationship = relationship.id;
    partner.partner = player.c.id;

    const success = breakUp(player, partner.id);

    expect(success).toBe(true);
    expect(player.relData).toHaveLength(0);
    expect(player.c.partner).toBeUndefined();
    expect((player.c as unknown as { relationship: string | null }).relationship).toBeNull();
    expect(partner.partner).toBeUndefined();
    expect((partner as unknown as { relationship: string | null }).relationship).toBeNull();
    expect(player.c.relationships).not.toContain('partner');
    expect(player.c.relationships).not.toContain('dating_match');
    expect(partner.relationships).not.toContain('partner');
    expect(partner.relationships).not.toContain('dating_match');
  });

  it('cleans up all relationship data on divorce', () => {
    const { player, partner } = createPlayerWithPartner();
    const relationship = createRelationship(
      player.c.id,
      partner.id,
      player.date,
      'Married',
      'You are married.'
    );

    player.relData.push(relationship as unknown as (typeof player.relData)[number]);
    (player.c as unknown as { relationship: string | null }).relationship = relationship.id;
    player.c.partner = partner.id;
    (partner as unknown as { relationship: string | null }).relationship = relationship.id;
    partner.partner = player.c.id;

    const success = divorce(player, partner.id);

    expect(success).toBe(true);
    expect(player.relData).toHaveLength(0);
    expect(player.c.partner).toBeUndefined();
    expect((player.c as unknown as { relationship: string | null }).relationship).toBeNull();
    expect(partner.partner).toBeUndefined();
    expect((partner as unknown as { relationship: string | null }).relationship).toBeNull();
  });

  describe('establishMatchRelationship (swipe match → active relationship)', () => {
    function createFreshMatch(): { player: Player; match: Person } {
      const character = new Person({
        id: 'player-char',
        firstname: 'Alex',
        lastname: 'Player',
        sex: 'Male',
        ageYears: 24,
      });
      // A freshly swiped match: tagged 'dating_match', no relData yet.
      const match = new Person({
        id: 'match-1',
        firstname: 'Stella',
        lastname: 'Thompson',
        sex: 'Female',
        ageYears: 24,
        affinity: 40,
        familiarity: 0,
        relationships: ['dating_match'],
      });
      const player = new Player({
        userId: 'user-1',
        character,
        r: [match],
        status: 'playing',
        date: '2026-02-07',
      });
      return { player, match };
    }

    it('creates an active Dating relationship the client can render', () => {
      const { player, match } = createFreshMatch();

      const rel = establishMatchRelationship(player, match);

      expect(rel).not.toBeNull();
      expect(rel?.relationshipStatus).toBe('Dating');
      // Lands in relData so the iOS Dating page finds an active relationship.
      expect(getActiveRelationship(player)?.id).toBe(rel?.id);
      // Scalar references wired up (serialized via Person.toJSON for the client).
      expect((player.c as unknown as { relationship: string | null }).relationship).toBe(rel?.id);
      expect((match as unknown as { relationship: string | null }).relationship).toBe(rel?.id);
      expect(player.c.partner).toBe(match.id);
      expect(match.partner).toBe(player.c.id);
      // 'dating_match' tag is swapped for 'partner'.
      expect(match.relationships).toContain('partner');
      expect(match.relationships).not.toContain('dating_match');
      expect(trackDatingMock).toHaveBeenCalledWith('user-1');
    });

    it('is idempotent for the same partner', () => {
      const { player, match } = createFreshMatch();

      const first = establishMatchRelationship(player, match);
      const second = establishMatchRelationship(player, match);

      expect(second?.id).toBe(first?.id);
      expect(player.relData).toHaveLength(1);
    });

    it('refuses to overwrite an existing active relationship with a different person', () => {
      const { player, match } = createFreshMatch();
      establishMatchRelationship(player, match);

      const other = new Person({
        id: 'match-2',
        firstname: 'Jordan',
        lastname: 'Rivers',
        sex: 'Female',
        ageYears: 25,
        relationships: ['dating_match'],
      });
      player.r.push(other);

      const result = establishMatchRelationship(player, other);

      expect(result).toBeNull();
      expect(player.relData).toHaveLength(1);
      expect(getActiveRelationship(player)?.person2).toBe(match.id);
    });
  });

  it('supports full progression via propose then marry', () => {
    const { player, partner } = createPlayerWithPartner();
    const relationship = createRelationship(
      player.c.id,
      partner.id,
      player.date,
      'Dating',
      'You are dating.'
    );
    player.relData.push(relationship as unknown as (typeof player.relData)[number]);

    expect(propose(player, partner.id)).toBe(true);
    expect(getRelData(player, partner.id)?.relationshipStatus).toBe('Engaged');

    expect(marry(player, partner.id)).toBe(true);
    expect(getRelData(player, partner.id)?.relationshipStatus).toBe('Married');
  });
});
