import { describe, expect, it, beforeEach, vi } from 'vitest';
import { GroupManager } from '../src/core/group-manager.js';
import { openDb, type Db } from '../src/db/index.js';

/** Insère un repo minimal directement (le GroupManager n'a besoin que de repos.id). */
function insertRepo(db: Db, id: string, path: string): void {
  db.prepare(
    "INSERT INTO repos (id, path, label, default_branch, post_create_hooks, pre_trust, created_at) VALUES (?, ?, ?, NULL, '[]', 0, ?)",
  ).run(id, path, id, new Date().toISOString());
}

describe('GroupManager', () => {
  let db: Db;
  let gm: GroupManager;

  beforeEach(() => {
    db = openDb(':memory:');
    insertRepo(db, 'repo-a', '/tmp/a');
    insertRepo(db, 'repo-b', '/tmp/b');
    gm = new GroupManager(db);
  });

  it('createGroup : groupe vide, persisté, événement group_update émis', () => {
    const spy = vi.fn();
    gm.on('group_update', spy);
    const g = gm.createGroup({ label: 'Sprint 42' });
    expect(g).toMatchObject({ label: 'Sprint 42', description: null, color: null, repoIds: [] });
    expect(gm.listGroups()).toHaveLength(1);
    expect(spy).toHaveBeenCalledWith(expect.objectContaining({ id: g.id }));
  });

  it('createGroup : repoIds initiaux ordonnés et dédoublonnés', () => {
    const g = gm.createGroup({ label: 'Eco', repoIds: ['repo-b', 'repo-a', 'repo-b'] });
    expect(g.repoIds).toEqual(['repo-b', 'repo-a']);
  });

  it('createGroup : repoId inexistant → 404', () => {
    expect(() => gm.createGroup({ label: 'X', repoIds: ['nope'] })).toThrow(
      expect.objectContaining({ statusCode: 404, code: 'REPO_NOT_FOUND' }),
    );
    expect(gm.listGroups()).toHaveLength(0); // rollback de la transaction
  });

  it('createGroup : validations (label vide / trop long, couleur invalide)', () => {
    expect(() => gm.createGroup({ label: '   ' })).toThrow(expect.objectContaining({ statusCode: 400 }));
    expect(() => gm.createGroup({ label: 'x'.repeat(101) })).toThrow(expect.objectContaining({ statusCode: 400 }));
    expect(() => gm.createGroup({ label: 'X', color: 'red' })).toThrow(expect.objectContaining({ statusCode: 400 }));
    expect(gm.createGroup({ label: 'X', color: '#4f46e5' }).color).toBe('#4f46e5');
  });

  it('getGroup : id inconnu → 404', () => {
    expect(() => gm.getGroup('nope')).toThrow(expect.objectContaining({ statusCode: 404, code: 'NOT_FOUND' }));
  });

  it('updateGroup : modifie label/description/color et bump updatedAt', () => {
    const g = gm.createGroup({ label: 'A' });
    const updated = gm.updateGroup(g.id, { label: 'B', description: 'hello', color: '#000000' });
    expect(updated).toMatchObject({ label: 'B', description: 'hello', color: '#000000' });
    expect(gm.updateGroup(g.id, { description: '' }).description).toBeNull(); // '' → null
  });

  it('addRepo : idempotent (PRIMARY KEY), émet group_update', () => {
    const g = gm.createGroup({ label: 'A' });
    const spy = vi.fn();
    gm.on('group_update', spy);
    expect(gm.addRepo(g.id, 'repo-a').repoIds).toEqual(['repo-a']);
    expect(gm.addRepo(g.id, 'repo-a').repoIds).toEqual(['repo-a']); // pas de doublon
    expect(gm.addRepo(g.id, 'repo-b').repoIds).toEqual(['repo-a', 'repo-b']);
    expect(spy).toHaveBeenCalledTimes(3);
  });

  it('addRepo : groupe ou repo inexistant → 404', () => {
    const g = gm.createGroup({ label: 'A' });
    expect(() => gm.addRepo('nope', 'repo-a')).toThrow(expect.objectContaining({ statusCode: 404, code: 'NOT_FOUND' }));
    expect(() => gm.addRepo(g.id, 'nope')).toThrow(expect.objectContaining({ statusCode: 404, code: 'REPO_NOT_FOUND' }));
  });

  it('removeRepo : idempotent (retrait d’un repo absent = no-op)', () => {
    const g = gm.createGroup({ label: 'A', repoIds: ['repo-a'] });
    expect(gm.removeRepo(g.id, 'repo-a').repoIds).toEqual([]);
    expect(gm.removeRepo(g.id, 'repo-a').repoIds).toEqual([]); // déjà absent → succès
  });

  it('deleteGroup : true + group_removed ; id inconnu → false', () => {
    const g = gm.createGroup({ label: 'A' });
    const spy = vi.fn();
    gm.on('group_removed', spy);
    expect(gm.deleteGroup(g.id)).toBe(true);
    expect(spy).toHaveBeenCalledWith(g.id);
    expect(gm.deleteGroup(g.id)).toBe(false);
    expect(gm.listGroups()).toHaveLength(0);
  });

  it('CASCADE : supprimer un repo purge la membership (PRAGMA foreign_keys = ON)', () => {
    const g = gm.createGroup({ label: 'A', repoIds: ['repo-a', 'repo-b'] });
    expect(gm.getGroup(g.id).repoIds).toEqual(['repo-a', 'repo-b']);
    db.prepare('DELETE FROM repos WHERE id = ?').run('repo-a');
    expect(gm.getGroup(g.id).repoIds).toEqual(['repo-b']);
  });
});
