import { describe, it, expect } from 'vitest';
import { validateRepoId, buildPullCommand, runModelPull } from '../model-pull.js';
import type { SshExec, ExecResult } from '../ssh-config.js';
import type { DeltaEmitter } from '../delta-emitter.js';

describe('validateRepoId', () => {
  it('accepts org/name', () => {
    expect(validateRepoId('Qwen/Qwen3.5-9B')).toBe(true);
    expect(validateRepoId('lmstudio-community/model.gguf-q4')).toBe(true);
  });
  it('rejects traversal, spaces, metacharacters, and bare names', () => {
    expect(validateRepoId('../etc/passwd')).toBe(false);
    expect(validateRepoId('a/b; rm -rf /')).toBe(false);
    expect(validateRepoId('a b/c')).toBe(false);
    expect(validateRepoId('justname')).toBe(false);
    expect(validateRepoId('a/b/c')).toBe(false);
  });
});

describe('buildPullCommand', () => {
  it('wrapper mode emits the pull verb', () => {
    expect(buildPullCommand('wrapper', 'Qwen/Q3')).toBe('pull Qwen/Q3');
  });
  it('shell mode emits huggingface-cli into a sanitized local dir', () => {
    expect(buildPullCommand('shell', 'Qwen/Q3', '/home/u/models/')).toBe(
      "huggingface-cli download Qwen/Q3 --local-dir '/home/u/models/Qwen__Q3'",
    );
  });
});

function emitterSpy(): { emitter: DeltaEmitter; frames: Record<string, unknown>[] } {
  const frames: Record<string, unknown>[] = [];
  const emitter: DeltaEmitter = {
    subscribe: () => () => {},
    publish: (d) => { frames.push(d as Record<string, unknown>); },
  };
  return { emitter, frames };
}

function execReturning(result: ExecResult): { exec: SshExec; calls: string[] } {
  const calls: string[] = [];
  const exec: SshExec = async (_t, command) => { calls.push(command); return result; };
  return { exec, calls };
}

const target = { host: 'h', user: 'u', keyPath: '/k' };

describe('runModelPull', () => {
  it('rejects an invalid repo id before issuing any command', async () => {
    const { emitter, frames } = emitterSpy();
    const { exec, calls } = execReturning({ code: 0, stdout: '', stderr: '' });
    const r = await runModelPull({ jobId: 'j1', providerId: 'test-provider', target, repo: '../x', mode: 'wrapper' }, exec, emitter);
    expect(r.ok).toBe(false);
    expect(calls).toHaveLength(0);
    expect(frames[frames.length - 1]).toMatchObject({ type: 'control_job', status: 'failed' });
  });

  it('runs the wrapper pull verb and emits running then completed', async () => {
    const { emitter, frames } = emitterSpy();
    const { exec, calls } = execReturning({ code: 0, stdout: 'done', stderr: '' });
    const r = await runModelPull({ jobId: 'j2', providerId: 'test-provider', target, repo: 'Qwen/Q3', mode: 'wrapper' }, exec, emitter);
    expect(r.ok).toBe(true);
    expect(calls).toEqual(['pull Qwen/Q3']);
    expect(frames.map((f) => f.status)).toEqual(['running', 'completed']);
    expect(frames.every((f) => (f.detail as { kind?: string }).kind === 'pull')).toBe(true);
  });

  it('reports a non-zero exit as failed', async () => {
    const { emitter, frames } = emitterSpy();
    const { exec } = execReturning({ code: 1, stdout: '', stderr: 'no such repo' });
    const r = await runModelPull({ jobId: 'j3', providerId: 'test-provider', target, repo: 'Qwen/Q3', mode: 'wrapper' }, exec, emitter);
    expect(r.ok).toBe(false);
    expect(frames[frames.length - 1]).toMatchObject({ status: 'failed' });
  });

  it('shell mode without a models dir fails fast', async () => {
    const { emitter } = emitterSpy();
    const { exec, calls } = execReturning({ code: 0, stdout: '', stderr: '' });
    const r = await runModelPull({ jobId: 'j4', providerId: 'test-provider', target, repo: 'Qwen/Q3', mode: 'shell' }, exec, emitter);
    expect(r.ok).toBe(false);
    expect(calls).toHaveLength(0);
  });
});
