import {
  mkdtempSync,
  readFileSync,
  rmSync,
  writeFileSync,
  existsSync,
} from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { CaseReportWorker, WorkerStart } from './case-report.worker';

//* 가짜 워커(dev/fake-worker, 표준 라이브러리 python3)로 하위 프로세스 실행기를 시험한다 — 명세 4.0 절 종료 코드 표
const FAKE_DIR = join(__dirname, 'dev', 'fake-worker');
const PASSWORD = 'synthetic-pw-7Q2';

describe('워커 실행기', () => {
  let root: string;
  const worker = new CaseReportWorker();
  const saved = { ...process.env };

  beforeAll(() => {
    root = mkdtempSync(join(tmpdir(), 'cr-worker-'));
    process.env.CASE_REPORT_WORKER_DIR = FAKE_DIR;
    process.env.CASE_REPORT_PYTHON = 'python3';
  });
  afterAll(() => {
    process.env = saved;
    rmSync(root, { recursive: true, force: true });
  });

  const start = (
    env: Record<string, string>,
    opts: Partial<WorkerStart> = {},
  ) => {
    worker.extraEnv = env;
    const taskDir = mkdtempSync(join(root, 'task-'));
    return {
      taskDir,
      run: worker.start({
        cmd: 'extract',
        taskDir,
        input: {
          schema: 'case-report.extract-input/1',
          file_id: 'f-1',
          kind_hint: null,
        },
        timeoutMs: 20_000,
        ...opts,
      }),
    };
  };

  it('정상 종료면 봉투를 돌려주고 PROGRESS 줄만 진행으로 넘긴다', async () => {
    const progress: unknown[] = [];
    const { run } = start({}, { onProgress: (p) => progress.push(p) });
    const out = await run.done;
    expect(out.failure).toBeNull();
    expect(out.envelope).toMatchObject({
      schema: 'case-report.extract/1',
      status: 'OK',
    });
    expect(out.envelope?.file?.file_id).toBe('f-1');
    expect(progress).toEqual(
      [0, 1, 2, 3].map((done) => ({ stage: 'OCR', done, total: 3 })),
    );
  });

  it.each([
    ['종료 코드 2 → 입력 오류', { FAKE_EXIT: '2' }, 'WORKER_INPUT_INVALID'],
    ['종료 코드 3 → 내부 오류', { FAKE_EXIT: '3' }, 'INTERNAL'],
    ['그 밖의 종료 코드 → 비정상 종료', { FAKE_EXIT: '9' }, 'WORKER_CRASHED'],
    [
      '0 인데 출력 없음 → 출력 깨짐',
      { FAKE_EXIT: 'NO_OUT' },
      'WORKER_BAD_OUTPUT',
    ],
    [
      '0 인데 JSON 아님 → 출력 깨짐',
      { FAKE_EXIT: 'BAD_JSON' },
      'WORKER_BAD_OUTPUT',
    ],
    [
      '0 인데 schema 다름 → 출력 깨짐',
      { FAKE_EXIT: 'BAD_SCHEMA' },
      'WORKER_BAD_OUTPUT',
    ],
  ])('%s', async (_name, env, failure) => {
    const out = await start(env).run.done;
    expect(out.envelope).toBeNull();
    expect(out.failure).toBe(failure);
  });

  it('시간 초과면 TIMEOUT 이고 손자 프로세스까지 종료한다', async () => {
    const pidFile = join(root, 'grandchild.pid');
    const out = await start(
      { FAKE_EXIT: 'GRANDCHILD', FAKE_GRANDCHILD_PID_FILE: pidFile },
      { timeoutMs: 1500 },
    ).run.done;
    expect(out.failure).toBe('TIMEOUT');
    const pid = Number(readFileSync(pidFile, 'utf8'));
    expect(pid).toBeGreaterThan(0);
    //* 좀비가 거둬질 때까지 잠깐 기다린다
    let alive = true;
    for (let i = 0; i < 30 && alive; i++) {
      try {
        process.kill(pid, 0);
        await new Promise((r) => setTimeout(r, 100));
      } catch (e) {
        alive = (e as NodeJS.ErrnoException).code !== 'ESRCH';
      }
    }
    expect(alive).toBe(false);
  });

  it('kill 하면 KILLED 로 끝난다', async () => {
    const { run } = start({ FAKE_EXIT: 'SLEEP' });
    setTimeout(() => run.kill(), 300);
    expect((await run.done).failure).toBe('KILLED');
  });

  it('비밀번호는 표준입력으로만 가고 입력 파일·표준오류 기록에 남지 않는다', async () => {
    const stderrFile = join(root, 'pw.stderr.txt');
    const ok = start(
      { FAKE_EXPECT_PASSWORD: PASSWORD },
      { password: PASSWORD, stderrFile },
    );
    expect((await ok.run.done).envelope?.status).toBe('OK');
    expect(readFileSync(join(ok.taskDir, 'in.json'), 'utf8')).not.toContain(
      PASSWORD,
    );
    expect(
      existsSync(stderrFile) ? readFileSync(stderrFile, 'utf8') : '',
    ).not.toContain(PASSWORD);

    const wrong = await start(
      { FAKE_EXPECT_PASSWORD: PASSWORD },
      { password: 'other' },
    ).run.done;
    expect(wrong.envelope).toMatchObject({
      status: 'NEEDS_PASSWORD',
      error: { code: 'PDF_PASSWORD_INCORRECT' },
    });
    const none = await start({ FAKE_EXPECT_PASSWORD: PASSWORD }).run.done;
    expect(none.envelope).toMatchObject({
      status: 'NEEDS_PASSWORD',
      error: { code: 'PDF_PASSWORD_REQUIRED' },
    });
  });

  it('AI 읽기 인증 env(CASE_REPORT_WIF)는 있을 때만 넘기고 다른 env 는 막는다', async () => {
    const names = async (tag: string) => {
      const file = join(root, `env-${tag}.json`);
      expect((await start({ FAKE_ENV_COPY: file }).run.done).failure).toBeNull();
      return JSON.parse(readFileSync(file, 'utf8')) as string[];
    };
    process.env.CR_SYNTH_OTHER = 'synthetic';
    delete process.env.CASE_REPORT_WIF;
    expect(await names('off')).not.toContain('CASE_REPORT_WIF');
    process.env.CASE_REPORT_WIF = 'c3ludGhldGlj';
    const on = await names('on');
    delete process.env.CASE_REPORT_WIF;
    expect(on).toContain('CASE_REPORT_WIF');
    expect(on).not.toContain('CR_SYNTH_OTHER');
  });

  it('표준오류는 워커 자신의 줄만 지정한 파일에 남기고 라이브러리 경고는 버린다', async () => {
    const stderrFile = join(root, 'err.txt');
    writeFileSync(stderrFile, '');
    await start(
      {
        FAKE_STDERR:
          'extract status=FAILED error=PDF_INVALID\nCannot set gray non-stroke color because /P0 is an invalid float value 김합성 010-1234-5678',
      },
      { stderrFile },
    ).run.done;
    const saved = readFileSync(stderrFile, 'utf8');
    expect(saved).toContain('extract status=FAILED error=PDF_INVALID');
    expect(saved).not.toContain('김합성');
    expect(saved).not.toContain('010-1234-5678');
    expect(saved).toContain('라이브러리 출력 1줄 버림');
  });
});
