import { describe, it, expect, vi } from 'vitest';
import { SessionDirector } from './session-director.service.js';
import { DbService } from './db.service.js';

function makeMockDb(overrides: Record<string, any> = {}): DbService {
  const mockQuery = vi.fn();
  return {
    pgClient: { query: mockQuery },
    getListenerBeliefs: vi.fn().mockResolvedValue([]),
    getLatestSessionState: vi.fn().mockResolvedValue(null),
    seedDefaultDiversityBudgets: vi.fn().mockResolvedValue(undefined),
    upsertDiversityBudget: vi.fn().mockResolvedValue(undefined),
    ...overrides,
  } as unknown as DbService;
}

describe('SessionDirector', () => {
  describe('pickArc', () => {
    const director = new SessionDirector(makeMockDb());

    it('returns late-night for low energy', () => {
      const arc = director.pickArc({ energy: 0.2, noveltyHunger: 0.3, sessionAgeMin: 10 } as any);
      expect(arc).toBe('late-night');
    });

    it('returns discovery for high energy + high novelty', () => {
      const arc = director.pickArc({ energy: 0.7, noveltyHunger: 0.6, sessionAgeMin: 5 } as any);
      expect(arc).toBe('discovery');
    });

    it('returns energetic for high energy + low novelty', () => {
      const arc = director.pickArc({ energy: 0.7, noveltyHunger: 0.3, sessionAgeMin: 5 } as any);
      expect(arc).toBe('energetic');
    });

    it('returns comfort for medium energy', () => {
      const arc = director.pickArc({ energy: 0.5, noveltyHunger: 0.3, sessionAgeMin: 10 } as any);
      expect(arc).toBe('comfort');
    });
  });

  describe('getArcSlots', () => {
    const director = new SessionDirector(makeMockDb());

    it('returns correct slot count', () => {
      expect(director.getArcSlots('comfort', 20).length).toBe(20);
      expect(director.getArcSlots('discovery', 20).length).toBe(20);
      expect(director.getArcSlots('energetic', 10).length).toBe(10);
      expect(director.getArcSlots('late-night', 8).length).toBe(8);
    });

    it('has valid role names', () => {
      const slots = director.getArcSlots('comfort', 20);
      const validRoles = ['known', 'adjacent', 'favorite', 'similar', 'new', 'medium', 'high', 'peak', 'cooldown', 'soft', 'ambient', 'acoustic', 'slow'];
      slots.forEach(s => expect(validRoles).toContain(s.role));
    });
  });

  describe('computeEntropy', () => {
    const director = new SessionDirector(makeMockDb());

    it('returns 0 for empty set', () => {
      expect(director.computeEntropy([])).toBe(0);
    });

    it('returns 1 for all-same-artist', () => {
      const candidates = [
        { trackId: 't1', generatorId: 'c', relevance: 1, explanation: [{ subjectType: 'artist', subjectId: 'a1', predicate: 'credited_main_on', objectType: 'track', objectId: 't1', fusedValue: 1 }] },
        { trackId: 't2', generatorId: 'c', relevance: 1, explanation: [{ subjectType: 'artist', subjectId: 'a1', predicate: 'credited_main_on', objectType: 'track', objectId: 't2', fusedValue: 1 }] },
      ] as any;
      expect(director.computeEntropy(candidates)).toBe(1);
    });

    it('returns ~0.5 for two-artist split', () => {
      const candidates = [
        { trackId: 't1', generatorId: 'c', relevance: 1, explanation: [{ subjectType: 'artist', subjectId: 'a1', predicate: 'credited_main_on', objectType: 'track', objectId: 't1', fusedValue: 1 }] },
        { trackId: 't2', generatorId: 'c', relevance: 1, explanation: [{ subjectType: 'artist', subjectId: 'a2', predicate: 'credited_main_on', objectType: 'track', objectId: 't2', fusedValue: 1 }] },
      ] as any;
      const hhi = director.computeEntropy(candidates);
      expect(hhi).toBeCloseTo(0.5);
    });
  });

  describe('rankCandidates', () => {
    it('sorts candidates by score descending', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValue({ rows: [] });
      const director = new SessionDirector(db);

      const candidates = [
        { trackId: 't1', generatorId: 'a', relevance: 0.9, explanation: [{ subjectType: 'artist', subjectId: 'a1', predicate: 'credited_main_on', objectType: 'track', objectId: 't1', fusedValue: 1 }] },
        { trackId: 't2', generatorId: 'b', relevance: 0.3, explanation: [{ subjectType: 'artist', subjectId: 'a2', predicate: 'credited_main_on', objectType: 'track', objectId: 't2', fusedValue: 1 }] },
      ];
      const fatigue = { artist: new Map(), genre: new Map(), track: new Map(), language: new Map(), vocal: 0 };
      const budgets = [{ dimension: 'artist', budgetShare: 0.2, horizonMin: 30, spent: 0 }];
      const state = { energy: 0.5, noveltyHunger: 0.3, sessionAgeMin: 10, lastArtistIds: [], lastGenreIds: [], context: null };

      const repetitionState = { recentTrackIds: new Set<string>(), recentArtistIds: new Set<string>() };
      const ranked = await director.rankCandidates(candidates, fatigue, budgets, state, repetitionState);
      expect(ranked[0].relevance).toBeGreaterThanOrEqual(ranked[ranked.length - 1].relevance);
    });
  });

  describe('buildState', () => {
    it('returns state with default values when no prior session', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValue({ rows: [] });
      (db.getListenerBeliefs as any).mockResolvedValue([]);
      (db.getLatestSessionState as any).mockResolvedValue(null);
      const director = new SessionDirector(db);
      const state = await director.buildState('user-1');
      expect(state).toHaveProperty('energy');
      expect(state).toHaveProperty('noveltyHunger');
      expect(state).toHaveProperty('sessionAgeMin');
      expect(typeof state.energy).toBe('number');
    });
  });
});
