jest.mock('drizzle/schema', () =>
  jest.requireActual('../../../../../drizzle/schema'),
);

import {
  DDX_ELIGIBLE_ORDER_STATE,
  DdxVerdictRepository,
} from './ddx-verdict.repository';

/**
 * 선정 질의의 `where` 절을 잡아 두는 하네스.
 *
 * drizzle 은 조건을 SQL 객체 트리로 쌓으므로, 실행 없이도 **어느 칼럼이 조건에 들어갔는지**를
 * 읽을 수 있다. DB 없이 물 수 있는 것은 거기까지다 — 이 검사는 값의 정확성이 아니라 **조건의
 * 존재**를 지킨다. 조기 종결(`empty`)은 조건 하나가 빠져서 생긴 결함이었고, 다시 빠지는 것을
 * 막는 것이 여기서 할 수 있는 일이다.
 */
const captureWhere = () => {
  const captured: { cond?: unknown } = {};
  const query: Record<string, unknown> = {};
  for (const m of ['from', 'leftJoin', 'orderBy', 'limit']) {
    query[m] = jest.fn(() => query);
  }
  query.where = jest.fn((cond: unknown) => {
    captured.cond = cond;
    return query;
  });
  query.then = (resolve: (v: unknown) => unknown) =>
    Promise.resolve([]).then(resolve);
  return { captured, drizzle: { select: jest.fn(() => query) } };
};

/** 조건 트리를 훑어 참조된 칼럼의 DB 이름을 모은다. */
const columnsIn = (node: unknown, acc: string[] = []): string[] => {
  if (!node || typeof node !== 'object') return acc;
  if (Array.isArray(node)) {
    for (const n of node) columnsIn(n, acc);
    return acc;
  }
  const n = node as { name?: unknown; table?: unknown; queryChunks?: unknown };
  if (typeof n.name === 'string' && n.table) acc.push(n.name);
  if (n.queryChunks) columnsIn(n.queryChunks, acc);
  return acc;
};

/** 조건 트리에 실린 바인딩 값을 모은다. */
const valuesIn = (node: unknown, acc: unknown[] = []): unknown[] => {
  if (!node || typeof node !== 'object') return acc;
  if (Array.isArray(node)) {
    for (const n of node) valuesIn(n, acc);
    return acc;
  }
  const n = node as { value?: unknown; queryChunks?: unknown };
  if ('value' in n && n.value !== undefined) acc.push(n.value);
  if (n.queryChunks) valuesIn(n.queryChunks, acc);
  return acc;
};

describe('DdxVerdictRepository.selectTargets 선정 조건', () => {
  const run = async () => {
    const { captured, drizzle } = captureWhere();
    const repo = new DdxVerdictRepository(drizzle as never);
    await repo.selectTargets(50);
    return {
      columns: columnsIn(captured.cond),
      values: valuesIn(captured.cond),
    };
  };

  it('완료 게이트 — 분석이 끝난 것만 고른다', async () => {
    const { columns, values } = await run();
    //* 이 두 줄이 이 파일의 존재 이유다. `odd_state` 조건이 빠지면 수집 중인 분석이 선정되고,
    //* 그 시점의 투약 그룹 0건이 `empty` 종결로 굳어 되살아나지 못한다.
    expect(columns).toContain('odd_state');
    expect(values).toContain(DDX_ELIGIBLE_ORDER_STATE);
  });

  it('완료 게이트가 인용하는 상태는 `분석완료` 다', () => {
    //* 값 자체를 여기 박지 않는다 — 정본은 `OrderDetailState` 이고, 이 검사는 선정이 그 열거의
    //* **어느 항목**을 쓰는지만 고정한다. 열거값이 바뀌면 두 자리가 함께 움직인다.
    expect(DDX_ELIGIBLE_ORDER_STATE).toBe('ODD006');
  });

  it('지운 분석은 고르지 않는다 — 실명이 외부로 새로 나가지 않는다', async () => {
    const { columns } = await run();
    expect(columns).toContain('odd_deletion');
  });

  it('기존 조건은 그대로 남는다 — 30일 창 · usrId · 판정 상태 · 시도 상한', async () => {
    const { columns } = await run();
    for (const c of [
      'odd_created_at',
      'usr_id',
      'ddv_id',
      'ddv_state',
      'ddv_attempts',
    ]) {
      expect(columns).toContain(c);
    }
  });
});
