import { describe, it, expect, vi } from 'vitest';
import { ALL_GENERATORS, type GeneratorContext, type Candidate } from './generators.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([]),
    ...overrides,
  } as unknown as DbService;
}

function makeCtx(overrides: Partial<GeneratorContext> = {}): GeneratorContext {
  return {
    userId: '00000000-0000-0000-0000-000000000000',
    seedTrackId: null,
    seedArtistId: null,
    beliefs: [],
    recentExclusions: [],
    toleranceMap: {},
    state: { energy: 0.5, lastArtistIds: [], lastGenreIds: [], context: null, noveltyHunger: 0.3, sessionAgeMin: 10 },
    ...overrides,
  };
}

// Index generators by name for easy test access
const generatorByName: Record<string, (typeof ALL_GENERATORS)[0]> = {
  comfort: ALL_GENERATORS[0],
  adjacent: ALL_GENERATORS[1],
  deepDive: ALL_GENERATORS[2],
  revival: ALL_GENERATORS[3],
  discovery: ALL_GENERATORS[4],
  novelty: ALL_GENERATORS[5],
  contextual: ALL_GENERATORS[6],
  experimental: ALL_GENERATORS[7],
};

describe('generators', () => {
  describe('comfort', () => {
    it('returns tracks for artists with affinity > 0.5', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValue({ rows: [{ id: 'track-1' }, { id: 'track-2' }] });
      const ctx = makeCtx({
        beliefs: [
          { entity_type: 'artist', entity_id: 'artist-1', value: 0.8, confidence: 0.9, profile: 'longterm', dimension: 'affinity' } as any,
        ],
      });
      const results = await generatorByName.comfort(db, ctx);
      expect(results.length).toBeGreaterThanOrEqual(1);
      expect(results[0]).toHaveProperty('trackId');
      expect(results[0]).toHaveProperty('generatorId', 'comfort');
      expect(results[0].explanation.length).toBeGreaterThanOrEqual(1);
    });

    it('returns empty when no high-affinity artists', async () => {
      const db = makeMockDb();
      const ctx = makeCtx({ beliefs: [ { entity_type: 'artist', entity_id: 'a1', value: 0.3, profile: 'longterm', dimension: 'affinity' } as any ] });
      const results = await generatorByName.comfort(db, ctx);
      expect(results).toHaveLength(0);
    });
  });

  describe('adjacent', () => {
    it('returns tracks from graph walks', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValueOnce({ rows: [{ reached_artist_id: 'artist-2' }] });
      (db.pgClient.query as any).mockResolvedValueOnce({ rows: [{ id: 'track-3' }] });
      const ctx = makeCtx({ seedTrackId: 'track-1', seedArtistId: 'artist-1' });
      const results = await generatorByName.adjacent(db, ctx);
      if (results.length > 0) {
        expect(results[0].explanation.length).toBeGreaterThanOrEqual(1);
      }
    });

    it('returns empty when no seed artist', async () => {
      const results = await generatorByName.adjacent(makeMockDb(), makeCtx());
      expect(results).toHaveLength(0);
    });
  });

  describe('discovery', () => {
    it('respects novelty_tolerance cap', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValue({ rows: [{ artist_id: 'a1' }, { artist_id: 'a2' }] });
      (db.pgClient.query as any).mockResolvedValue({ rows: [{ id: 't1' }] });
      const ctx = makeCtx({
        beliefs: [ { entity_type: 'artist', entity_id: 'trusted-1', value: 0.5, confidence: 0.8, profile: 'longterm', dimension: 'affinity' } as any ],
        toleranceMap: { novelty_tolerance: 0.1 },
      });
      const results = await generatorByName.discovery(db, ctx);
      const maxCandidates = Math.max(2, Math.floor(20 * 0.1));
      expect(results.length).toBeLessThanOrEqual(maxCandidates);
    });
  });

  describe('deepDive', () => {
    it('returns album-ordered tracks for obsession artists', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValueOnce({ rows: [{ album_id: 'alb-1', artist_id: 'a1', title: 'Album 1' }] });
      (db.pgClient.query as any).mockResolvedValue({ rows: [{ id: 't1' }, { id: 't2' }] });
      const ctx = makeCtx({
        beliefs: [ { entity_type: 'artist', entity_id: 'a1', dimension: 'affinity', value: 0.6, profile: 'obsession' } as any ],
      });
      const results = await generatorByName.deepDive(db, ctx);
      expect(results.length).toBeGreaterThanOrEqual(1);
      expect(results[0].generatorId).toBe('deep-dive');
    });
  });

  describe('revival', () => {
    it('returns tracks for stale high-affinity artists', async () => {
      const db = makeMockDb();
      // First query: stale listener_beliefs
      (db.pgClient.query as any).mockResolvedValueOnce({
        rows: [{ artist_id: 'a1', affinity: 0.7 }]
      });
      // Second query: tracks by those artists
      (db.pgClient.query as any).mockResolvedValue({
        rows: [{ id: 't1' }]
      });
      const ctx = makeCtx({
        beliefs: [ { entity_type: 'artist', entity_id: 'a1', dimension: 'affinity', value: 0.5, profile: 'forgotten' } as any ],
      });
      const results = await generatorByName.revival(db, ctx);
      expect(results.length).toBeGreaterThanOrEqual(1);
      expect(results[0].generatorId).toBe('revival');
    });
  });
  describe('novelty', () => {
    it('returns recent tracks', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValue({ rows: [{ id: 't1' }] });
      const ctx = makeCtx({
        beliefs: [ { entity_type: 'artist', entity_id: 'trusted-1', value: 0.5, profile: 'longterm', dimension: 'affinity' } as any ],
      });
      const results = await generatorByName.novelty(db, ctx);
      if (results.length > 0) {
        expect(results[0].generatorId).toBe('novelty');
      }
    });
  });

  describe('experimental', () => {
    it('returns tracks from unfamiliar genres', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValue({ rows: [{ id: 't1', genre_id: 'g1' }] });
      const ctx = makeCtx({
        beliefs: [ { entity_type: 'artist', entity_id: 'a1', value: 0.6, profile: 'longterm', dimension: 'affinity' } as any ],
      });
      const results = await generatorByName.experimental(db, ctx);
      expect(results).toBeDefined();
    });
  });

  describe('contextual', () => {
    it('returns tracks matching context when set', async () => {
      const db = makeMockDb();
      (db.pgClient.query as any).mockResolvedValueOnce({ rows: [{ id: 't1' }] });
      const ctx = makeCtx({
        state: { energy: 0.5, lastArtistIds: [], lastGenreIds: [], context: 'coding', noveltyHunger: 0.3, sessionAgeMin: 10 },
      });
      const results = await generatorByName.contextual(db, ctx);
      expect(results).toBeDefined();
    });

    it('returns empty when no context set', async () => {
      const results = await generatorByName.contextual(makeMockDb(), makeCtx());
      expect(results).toHaveLength(0);
    });
  });
});

describe('ALL_GENERATORS', () => {
  it('contains 8 generators', () => {
    expect(ALL_GENERATORS).toHaveLength(8);
    ALL_GENERATORS.forEach(g => expect(typeof g).toBe('function'));
  });
});
