/**
 * copyChecklistToSplit 단위 테스트
 *
 * 대상: /src/controllers/popProductionController.ts 의 copyChecklistToSplit
 *
 * 전략: 실제 DB 연결 없이 client.query 를 Jest mock 으로 주입한다.
 * 테스트 대상 함수는 외부에서 주입된 client 만을 사용하여 쿼리를 실행하므로
 * pg/Pool 전체를 모킹할 필요가 없다 (순수한 query router 로직 검증).
 *
 * 커버 분기:
 *   - A-1  wi_* 커스텀 템플릿 존재 (wi_process_work_item) -> wi_* 에서 복사
 *   - A-2  wi_* 미존재 또는 workInstructionNo 미지정 -> 원본 process_work_item 에서 복사
 *   - skipAStrategy=true -> A 전략 전체 skip, B 전략 진입
 *   - A 에서 0 건 -> B 전략 fallthrough
 *   - routingDetailId=null -> 곧장 B 전략
 *   - B 전략 = 마스터 wop 의 기존 process_work_result 구조 복사
 */

import { copyChecklistToSplit } from "../popProductionController";

type QueryCall = { text: string; values?: unknown[] };

/**
 * client.query mock 헬퍼.
 * calls 배열에 호출 인자를 순서대로 저장하고, responses 큐에서 응답을 순서대로 반환한다.
 * responses 가 고갈되면 기본값 { rows: [], rowCount: 0 } 을 반환한다.
 */
function makeClient(
  responses: Array<{ rows?: unknown[]; rowCount?: number }>,
): {
  client: { query: jest.Mock };
  calls: QueryCall[];
} {
  const calls: QueryCall[] = [];
  const queue = [...responses];
  const client = {
    query: jest.fn((text: string, values?: unknown[]) => {
      calls.push({ text, values });
      const next = queue.shift();
      return Promise.resolve(next ?? { rows: [], rowCount: 0 });
    }),
  };
  return { client, calls };
}

const COMPANY = "TESTCO";
const USER = "tester01";
const MASTER_WOP = "master-wop-id";
const WOP_RESULT = "wop-result-id";
const ROUTING_DETAIL = "routing-detail-id";
const WI_NO = "WI-20260424-001";

describe("copyChecklistToSplit", () => {
  describe("A-1: wi_* 커스텀 템플릿 우선 복사", () => {
    it("workInstructionNo 지정 + wi_process_work_item row 존재 시 wi_* 템플릿에서 복사한다", async () => {
      const { client, calls } = makeClient([
        { rows: [{ "?column?": 1 }], rowCount: 1 },
        { rows: [], rowCount: 3 },
      ]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
        { workInstructionNo: WI_NO },
      );

      expect(inserted).toBe(3);
      expect(client.query).toHaveBeenCalledTimes(2);

      expect(calls[0].text).toContain("FROM wi_process_work_item");
      expect(calls[0].values).toEqual([
        WI_NO,
        ROUTING_DETAIL,
        COMPANY,
      ]);

      expect(calls[1].text).toContain("INSERT INTO process_work_result");
      expect(calls[1].text).toContain("FROM wi_process_work_item wi");
      expect(calls[1].text).toContain("wi_process_work_item_detail wid");
      expect(calls[1].values).toEqual([
        WOP_RESULT,
        USER,
        ROUTING_DETAIL,
        COMPANY,
        WI_NO,
      ]);
    });

    it("A-1 결과가 0건이면 B 전략으로 fallthrough 하여 마스터 스냅샷에서 복사한다", async () => {
      const { client, calls } = makeClient([
        { rows: [{ "?column?": 1 }], rowCount: 1 },
        { rows: [], rowCount: 0 },
        { rows: [], rowCount: 7 },
      ]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
        { workInstructionNo: WI_NO },
      );

      expect(inserted).toBe(7);
      expect(client.query).toHaveBeenCalledTimes(3);
      expect(calls[2].text).toContain("FROM process_work_result");
      expect(calls[2].text).toContain("WHERE work_order_process_id = $3");
      expect(calls[2].values).toEqual([
        WOP_RESULT,
        USER,
        MASTER_WOP,
        COMPANY,
      ]);
    });
  });

  describe("A-2: 원본 템플릿 fallback", () => {
    it("workInstructionNo 미지정 시 원본 process_work_item 에서 복사한다", async () => {
      const { client, calls } = makeClient([{ rows: [], rowCount: 5 }]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
      );

      expect(inserted).toBe(5);
      expect(client.query).toHaveBeenCalledTimes(1);
      expect(calls[0].text).toContain("FROM process_work_item pwi");
      expect(calls[0].text).toContain("process_work_item_detail pwd");
      expect(calls[0].text).not.toContain("wi_process_work_item");
      expect(calls[0].values).toEqual([
        WOP_RESULT,
        USER,
        ROUTING_DETAIL,
        COMPANY,
      ]);
    });

    it("workInstructionNo 지정됐지만 wi_* row 가 0개면 원본 템플릿에서 복사한다", async () => {
      const { client, calls } = makeClient([
        { rows: [], rowCount: 0 },
        { rows: [], rowCount: 4 },
      ]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
        { workInstructionNo: WI_NO },
      );

      expect(inserted).toBe(4);
      expect(client.query).toHaveBeenCalledTimes(2);
      expect(calls[0].text).toContain("SELECT 1 FROM wi_process_work_item");
      expect(calls[1].text).toContain("FROM process_work_item pwi");
      expect(calls[1].text).not.toContain("wi_process_work_item");
    });

    it("A-2 결과가 0건이면 B 전략으로 fallthrough 한다", async () => {
      const { client, calls } = makeClient([
        { rows: [], rowCount: 0 },
        { rows: [], rowCount: 2 },
      ]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
      );

      expect(inserted).toBe(2);
      expect(client.query).toHaveBeenCalledTimes(2);
      expect(calls[1].text).toContain("FROM process_work_result");
      expect(calls[1].text).toContain("WHERE work_order_process_id = $3");
    });
  });

  describe("skipAStrategy: A 전략 전체 건너뛰기", () => {
    it("skipAStrategy=true 이면 routingDetailId 와 workInstructionNo 가 있어도 B 전략만 실행한다", async () => {
      const { client, calls } = makeClient([{ rows: [], rowCount: 10 }]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
        { workInstructionNo: WI_NO, skipAStrategy: true },
      );

      expect(inserted).toBe(10);
      expect(client.query).toHaveBeenCalledTimes(1);
      expect(calls[0].text).toContain("FROM process_work_result");
      expect(calls[0].text).toContain("WHERE work_order_process_id = $3");
      expect(calls[0].text).not.toContain("wi_process_work_item");
      expect(calls[0].text).not.toContain("process_work_item pwi");
      expect(calls[0].values).toEqual([
        WOP_RESULT,
        USER,
        MASTER_WOP,
        COMPANY,
      ]);
    });

    it("skipAStrategy=false (명시) 는 기본 동작과 동일하다", async () => {
      const { client } = makeClient([{ rows: [], rowCount: 2 }]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
        { skipAStrategy: false },
      );

      expect(inserted).toBe(2);
      expect(client.query).toHaveBeenCalledTimes(1);
    });
  });

  describe("B: routingDetailId 없음", () => {
    it("routingDetailId=null 이면 A 전략 skip, 곧장 B 전략 실행", async () => {
      const { client, calls } = makeClient([{ rows: [], rowCount: 6 }]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        null,
        COMPANY,
        USER,
      );

      expect(inserted).toBe(6);
      expect(client.query).toHaveBeenCalledTimes(1);
      expect(calls[0].text).toContain("FROM process_work_result");
      expect(calls[0].values).toEqual([
        WOP_RESULT,
        USER,
        MASTER_WOP,
        COMPANY,
      ]);
    });

    it("routingDetailId=null + workInstructionNo 지정은 workInstructionNo 무시하고 B 전략 실행", async () => {
      const { client, calls } = makeClient([{ rows: [], rowCount: 1 }]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        null,
        COMPANY,
        USER,
        { workInstructionNo: WI_NO },
      );

      expect(inserted).toBe(1);
      expect(client.query).toHaveBeenCalledTimes(1);
      expect(calls[0].text).not.toContain("wi_process_work_item");
      expect(calls[0].text).toContain("FROM process_work_result");
    });
  });

  describe("엣지 케이스", () => {
    it("B 전략에서도 0 건이면 0 을 반환한다", async () => {
      const { client } = makeClient([{ rows: [], rowCount: 0 }]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        null,
        COMPANY,
        USER,
      );

      expect(inserted).toBe(0);
    });

    it("rowCount 가 undefined 면 0 을 반환한다 (null safety)", async () => {
      const { client } = makeClient([{ rows: [] }]);

      const inserted = await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        null,
        COMPANY,
        USER,
      );

      expect(inserted).toBe(0);
    });

    it("wi_* 체크 쿼리에 company_code 가 필터로 포함된다 (멀티테넌시)", async () => {
      const { client, calls } = makeClient([
        { rows: [], rowCount: 0 },
        { rows: [], rowCount: 1 },
      ]);

      await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
        { workInstructionNo: WI_NO },
      );

      expect(calls[0].text).toContain("company_code = $3");
      expect(calls[0].values?.[2]).toBe(COMPANY);
    });

    it("모든 INSERT 쿼리는 파라미터 바인딩을 사용한다 (문자열 삽입 금지)", async () => {
      const { client, calls } = makeClient([
        { rows: [{ "?column?": 1 }], rowCount: 1 },
        { rows: [], rowCount: 1 },
      ]);

      await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        ROUTING_DETAIL,
        COMPANY,
        USER,
        { workInstructionNo: WI_NO },
      );

      // 모든 호출에서 values 가 존재하고 query 텍스트에 placeholder 가 있어야 한다
      for (const call of calls) {
        expect(call.values).toBeDefined();
        expect(Array.isArray(call.values)).toBe(true);
        expect(call.text).toMatch(/\$\d+/);
      }
    });

    it("B 전략은 항상 masterProcessId 로 소스 스냅샷을 조회한다", async () => {
      const { client, calls } = makeClient([{ rows: [], rowCount: 3 }]);

      await copyChecklistToSplit(
        client,
        MASTER_WOP,
        WOP_RESULT,
        null,
        COMPANY,
        USER,
      );

      const insertSql = calls[0].text;
      expect(insertSql).toContain("FROM process_work_result");
      expect(insertSql).toContain("WHERE work_order_process_id = $3");
      expect(calls[0].values?.[2]).toBe(MASTER_WOP);
      expect(calls[0].values?.[3]).toBe(COMPANY);
    });
  });
});
