import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Repository } from './repository.js';

let tempDir = '';
let repo: Repository;

beforeEach(() => {
  tempDir = mkdtempSync(join(tmpdir(), 'maestro-pkgreq-'));
  repo = new Repository(join(tempDir, 'db.sqlite'));
});
afterEach(() => {
  repo.close();
  rmSync(tempDir, { recursive: true, force: true });
});

describe('package_requests repository', () => {
  it('records, fetches and lists a request', () => {
    const id = repo.recordPackageRequest({
      taskId: '100', jobId: 'job-1', spaceId: 'space-1',
      pieceName: 'chat', movementName: 'work',
      spec: 'requests==2.32.3', normalizedName: 'requests', reason: 'http',
    });
    const got = repo.getPackageRequest(id);
    expect(got?.spec).toBe('requests==2.32.3');
    expect(got?.normalizedName).toBe('requests');
    expect(got?.status).toBe('pending');
    expect(got?.spaceId).toBe('space-1');
    expect(got?.movementName).toBe('work');

    const list = repo.listPackageRequestsByTask('100');
    expect(list).toHaveLength(1);
    expect(list[0].id).toBe(id);
  });

  it('de-dups an identical still-pending request within the same job', () => {
    const a = repo.recordPackageRequest({ taskId: '1', jobId: 'j', spec: 'requests', normalizedName: 'requests' });
    const b = repo.recordPackageRequest({ taskId: '1', jobId: 'j', spec: 'requests', normalizedName: 'requests' });
    expect(b).toBe(a);
    expect(repo.listPackageRequestsByTask('1')).toHaveLength(1);
  });

  it('does NOT de-dup once the first is decided (a fresh request can be raised)', () => {
    const a = repo.recordPackageRequest({ taskId: '1', jobId: 'j', spec: 'numpy', normalizedName: 'numpy' });
    expect(repo.decidePackageRequest(a, { status: 'denied', decidedBy: 'u1' })).toBe(true);
    const b = repo.recordPackageRequest({ taskId: '1', jobId: 'j', spec: 'numpy', normalizedName: 'numpy' });
    expect(b).not.toBe(a);
    expect(repo.listPackageRequestsByTask('1')).toHaveLength(2);
  });

  it('decide is a CAS: a second decide on the same row is a no-op', () => {
    const id = repo.recordPackageRequest({ taskId: '1', jobId: 'j', spec: 'pandas', normalizedName: 'pandas' });
    expect(repo.decidePackageRequest(id, { status: 'approved', decidedBy: 'u1' })).toBe(true);
    expect(repo.decidePackageRequest(id, { status: 'denied', decidedBy: 'u2' })).toBe(false);
    const got = repo.getPackageRequest(id);
    expect(got?.status).toBe('approved');
    expect(got?.decidedBy).toBe('u1');
    expect(got?.decidedAt).not.toBeNull();
  });

  it('revertPackageRequestToPending un-claims an approved request (for install-failure retry)', () => {
    const id = repo.recordPackageRequest({ taskId: '1', jobId: 'j', spec: 'httpx', normalizedName: 'httpx' });
    // claim
    expect(repo.decidePackageRequest(id, { status: 'approved', decidedBy: 'u1' })).toBe(true);
    // revert (install failed)
    expect(repo.revertPackageRequestToPending(id)).toBe(true);
    const got = repo.getPackageRequest(id);
    expect(got?.status).toBe('pending');
    expect(got?.decidedBy).toBeNull();
    expect(got?.decidedAt).toBeNull();
    // can be claimed again
    expect(repo.decidePackageRequest(id, { status: 'approved', decidedBy: 'u2' })).toBe(true);
    // revert only touches 'approved' rows: a denied row is untouched
    const denied = repo.recordPackageRequest({ taskId: '1', jobId: 'k', spec: 'numpy', normalizedName: 'numpy' });
    repo.decidePackageRequest(denied, { status: 'denied', decidedBy: 'u1' });
    expect(repo.revertPackageRequestToPending(denied)).toBe(false);
    expect(repo.getPackageRequest(denied)?.status).toBe('denied');
  });

  it('listDeclinedPackageNamesByJob returns denied + auto_denied names for the job only', () => {
    const denied = repo.recordPackageRequest({ taskId: '1', jobId: 'jA', spec: 'httpx', normalizedName: 'httpx' });
    repo.decidePackageRequest(denied, { status: 'denied', decidedBy: 'u1' });
    const auto = repo.recordPackageRequest({ taskId: '1', jobId: 'jA', spec: 'boto3', normalizedName: 'boto3', status: 'auto_denied' });
    expect(auto).toBeTruthy();
    // pending + other-job requests are excluded
    repo.recordPackageRequest({ taskId: '1', jobId: 'jA', spec: 'numpy', normalizedName: 'numpy' });
    repo.recordPackageRequest({ taskId: '1', jobId: 'jB', spec: 'flask', normalizedName: 'flask', status: 'denied' });
    const names = repo.listDeclinedPackageNamesByJob('jA').sort();
    expect(names).toEqual(['boto3', 'httpx']);
  });

  it('resumePackageRequestJob only re-queues a job parked for package_request', () => {
    // No such job → 0 changes (safe no-op).
    expect(repo.resumePackageRequestJob('missing-job')).toBe(0);
  });
});
