import { describe, expect, it, vi } from "vitest";

vi.mock("@techstark/opencv-js", () => ({
  default: {
    Mat() {}
  }
}));

import { PaddleOCR, normalizeOcrPipelineConfig, parseOcrPipelineConfigText } from "../src/index";
import { extractInferenceModelName } from "../src/models/common";
import { DEFAULT_OCR_PIPELINE_CONFIG_TEXT } from "../src/pipelines/ocr/default-config";
import { normalizeOrtOptions } from "../src/pipelines/ocr/shared";
import { getOcrRuntimeParams } from "../src/pipelines/ocr/runtime-params";

const CREATE_WITHOUT_INIT = Object.freeze({
  initialize: false
});

const IGNORE_UNSUPPORTED = Object.freeze({
  initialize: false,
  unsupportedBehavior: "ignore"
});

const pipelineConfigText = `
pipeline_name: OCR
use_doc_preprocessor: true
use_textline_orientation: true
SubModules:
  TextDetection:
    model_name: PP-OCRv5_mobile_det
    limit_side_len: 64
    limit_type: min
    max_side_limit: 4000
    thresh: 0.3
    box_thresh: 0.6
    unclip_ratio: 1.5
  TextLineOrientation:
    model_name: PP-LCNet_x1_0_textline_ori
  TextRecognition:
    model_name: PP-OCRv5_mobile_rec
    score_thresh: 0.0
`;

const customDetAsset = {
  url: "https://example.com/custom-det.tar"
};

const customRecAsset = {
  url: "https://example.com/custom-rec.tar"
};

class MockWorker {
  constructor(responder) {
    this.responder = responder;
    this.messages = [];
    this.terminated = false;
    this.onmessage = null;
    this.onerror = null;
  }

  postMessage(message) {
    this.messages.push(message);
    queueMicrotask(() => {
      if (this.terminated) return;
      const response = this.responder(message);
      if (!response) return;
      this.onmessage?.({ data: response });
    });
  }

  terminate() {
    this.terminated = true;
  }
}

function expectDefaultModelAssets(ocr) {
  expect(ocr.options.pipelineConfig.assets.det?.url).toMatch(/PP-OCRv5_mobile_det/);
  expect(ocr.options.pipelineConfig.assets.rec?.url).toMatch(/PP-OCRv5_mobile_rec/);
}

describe("PaddleOCR high-level API", () => {
  it("parses and normalizes OCR pipeline configs", () => {
    const parsed = parseOcrPipelineConfigText(pipelineConfigText);
    const normalized = normalizeOcrPipelineConfig(parsed);

    expect(normalized.pipelineName).toBe("OCR");
    expect(normalized.modelSelection.textDetectionModelName).toBe("PP-OCRv5_mobile_det");
    expect(normalized.modelSelection.textRecognitionModelName).toBe("PP-OCRv5_mobile_rec");
    expect(normalized.modelSelection).not.toHaveProperty("detAsset");
    expect(normalized.modelSelection).not.toHaveProperty("recAsset");
    expect(normalized.runtimeDefaults.text_det_limit_side_len).toBe(64);
    expect(normalized.runtimeDefaults.text_det_limit_type).toBe("min");
    expect(normalized.warnings).toHaveLength(2);
  });

  it("keeps pipeline-declared custom assets separate from model selection", () => {
    const normalized = normalizeOcrPipelineConfig({
      pipeline_name: "OCR",
      SubModules: {
        TextDetection: {
          model_name: "custom_det",
          model_dir: customDetAsset
        },
        TextRecognition: {
          model_name: "custom_rec",
          model_dir: customRecAsset
        }
      }
    });

    expect(normalized.modelSelection.textDetectionModelName).toBe("custom_det");
    expect(normalized.modelSelection.textRecognitionModelName).toBe("custom_rec");
    expect(normalized.assets.det.url).toBe("https://example.com/custom-det.tar");
    expect(normalized.assets.rec.url).toBe("https://example.com/custom-rec.tar");
  });

  it("creates an OCR instance from lang and ocrVersion", async () => {
    const ocr = await PaddleOCR.create({
      lang: "ch",
      ocrVersion: "PP-OCRv5",
      ...CREATE_WITHOUT_INIT
    });

    expect(ocr).toBeInstanceOf(PaddleOCR);
    expectDefaultModelAssets(ocr);
  });

  it("keeps the same create API when worker mode is enabled", async () => {
    const defaultOrt = normalizeOrtOptions();
    const ocr = await PaddleOCR.create({
      lang: "ch",
      ocrVersion: "PP-OCRv5",
      worker: true,
      ...CREATE_WITHOUT_INIT
    });

    expect(typeof ocr.initialize).toBe("function");
    expect(typeof ocr.predict).toBe("function");
    expect(typeof ocr.dispose).toBe("function");
    expect(ocr.options.pipelineConfig.assets.det?.url).toMatch(/PP-OCRv5_mobile_det/);
    expect(ocr.options.ortOptions.backend).toBe(defaultOrt.backend);
  });

  it("uses the package OCR.yaml defaults when no pipeline config is passed", async () => {
    const defaultPipeline = normalizeOcrPipelineConfig(DEFAULT_OCR_PIPELINE_CONFIG_TEXT);
    const ocr = await PaddleOCR.create(CREATE_WITHOUT_INIT);

    expect(ocr.options.pipelineConfig.assets.det?.url).toMatch(
      new RegExp(defaultPipeline.modelSelection.textDetectionModelName)
    );
    expect(ocr.options.pipelineConfig.assets.rec?.url).toMatch(
      new RegExp(defaultPipeline.modelSelection.textRecognitionModelName)
    );
    expect(ocr.pipelineConfig.runtimeDefaults).toMatchObject(defaultPipeline.runtimeDefaults);
  });

  it("maps English PP-OCRv5 selection to the mobile model set", async () => {
    const ocr = await PaddleOCR.create({
      lang: "en",
      ocrVersion: "PP-OCRv5",
      ...CREATE_WITHOUT_INIT
    });

    expectDefaultModelAssets(ocr);
  });

  it("maps PP-OCRv6 lang selection to the small model set", async () => {
    const ocr = await PaddleOCR.create({
      lang: "ch",
      ocrVersion: "PP-OCRv6",
      ...CREATE_WITHOUT_INIT
    });

    expect(ocr.options.pipelineConfig.assets.det?.url).toMatch(/PP-OCRv6_small_det/);
    expect(ocr.options.pipelineConfig.assets.rec?.url).toMatch(/PP-OCRv6_small_rec/);
    expect(ocr.options.pipelineConfig.modelSelection.textDetectionModelName).toBe(
      "PP-OCRv6_small_det"
    );
    expect(ocr.options.pipelineConfig.modelSelection.textRecognitionModelName).toBe(
      "PP-OCRv6_small_rec"
    );
  });

  it("maps PP-OCRv6 Latin languages to the small model set", async () => {
    const ocr = await PaddleOCR.create({
      lang: "fr",
      ocrVersion: "PP-OCRv6",
      ...CREATE_WITHOUT_INIT
    });

    expect(ocr.options.pipelineConfig.assets.det?.url).toMatch(/PP-OCRv6_small_det/);
    expect(ocr.options.pipelineConfig.assets.rec?.url).toMatch(/PP-OCRv6_small_rec/);
  });

  it("rejects PP-OCRv6 with languages outside the v6 coverage", async () => {
    for (const lang of ["ru", "pi"]) {
      await expect(
        PaddleOCR.create({
          lang,
          ocrVersion: "PP-OCRv6",
          ...CREATE_WITHOUT_INIT
        })
      ).rejects.toThrow(/Unsupported lang\/ocrVersion combination/);
    }
  });

  it("maps PP-OCRv6 az and ku lang selection to the small model set", async () => {
    for (const lang of ["az", "ku"]) {
      const ocr = await PaddleOCR.create({
        lang,
        ocrVersion: "PP-OCRv6",
        ...CREATE_WITHOUT_INIT
      });

      expect(ocr.options.pipelineConfig.assets.det?.url).toMatch(/PP-OCRv6_small_det/);
      expect(ocr.options.pipelineConfig.assets.rec?.url).toMatch(/PP-OCRv6_small_rec/);
    }
  });

  it("resolves PP-OCRv6 tiny models via explicit model names", async () => {
    const ocr = await PaddleOCR.create({
      text_detection_model_name: "PP-OCRv6_tiny_det",
      text_recognition_model_name: "PP-OCRv6_tiny_rec",
      ...CREATE_WITHOUT_INIT
    });

    expect(ocr.options.pipelineConfig.assets.det?.url).toMatch(/PP-OCRv6_tiny_det_onnx_infer\.tar$/);
    expect(ocr.options.pipelineConfig.assets.rec?.url).toMatch(/PP-OCRv6_tiny_rec_onnx_infer\.tar$/);
  });

  it("allows overriding model selection via model_name options", async () => {
    const ocr = await PaddleOCR.create({
      text_detection_model_name: "PP-OCRv5_mobile_det",
      text_recognition_model_name: "PP-OCRv5_mobile_rec",
      ...CREATE_WITHOUT_INIT
    });

    expectDefaultModelAssets(ocr);
  });

  it("creates an OCR instance from pipeline config model names", async () => {
    const ocr = await PaddleOCR.create({
      pipelineConfig: pipelineConfigText,
      ...IGNORE_UNSUPPORTED
    });

    expectDefaultModelAssets(ocr);
    expect(ocr.pipelineConfig.runtimeDefaults.text_det_limit_type).toBe("min");
    expect(ocr.pipelineConfig.runtimeDefaults.text_rec_score_thresh).toBe(0);
  });

  it("lets explicit model assets override pipeline config model names", async () => {
    const ocr = await PaddleOCR.create({
      pipelineConfig: pipelineConfigText,
      text_detection_model_name: "custom_det",
      textDetectionModelAsset: customDetAsset,
      text_recognition_model_name: "custom_rec",
      textRecognitionModelAsset: customRecAsset,
      ...IGNORE_UNSUPPORTED
    });

    expect(ocr.options.pipelineConfig.assets.det?.url).toBe("https://example.com/custom-det.tar");
    expect(ocr.options.pipelineConfig.assets.rec?.url).toBe("https://example.com/custom-rec.tar");
  });

  it("allows overriding only one side with a custom model asset", async () => {
    const ocr = await PaddleOCR.create({
      pipelineConfig: pipelineConfigText,
      text_detection_model_name: "custom_det",
      textDetectionModelAsset: customDetAsset,
      ...IGNORE_UNSUPPORTED
    });

    expect(ocr.options.pipelineConfig.assets.det?.url).toBe("https://example.com/custom-det.tar");
    expect(ocr.options.pipelineConfig.assets.rec?.url).toMatch(/PP-OCRv5_mobile_rec/);
  });

  it("lets explicit model names override pipeline config model assets", async () => {
    const ocr = await PaddleOCR.create({
      pipelineConfig: {
        pipeline_name: "OCR",
        SubModules: {
          TextDetection: {
            model_name: "custom_det",
            model_dir: customDetAsset
          },
          TextRecognition: {
            model_name: "custom_rec",
            model_dir: customRecAsset
          }
        }
      },
      text_detection_model_name: "PP-OCRv5_mobile_det",
      text_recognition_model_name: "PP-OCRv5_mobile_rec",
      ...CREATE_WITHOUT_INIT
    });

    expectDefaultModelAssets(ocr);
  });

  it("rejects unsupported lang/ocrVersion combinations", async () => {
    await expect(
      PaddleOCR.create({
        lang: "kl",
        ocrVersion: "PP-OCRv5",
        ...CREATE_WITHOUT_INIT
      })
    ).rejects.toThrow(/Unsupported lang\/ocrVersion combination/i);
  });

  it("warns about unsupported pipeline features by default", async () => {
    const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
    await PaddleOCR.create({ pipelineConfig: pipelineConfigText, ...CREATE_WITHOUT_INIT });

    expect(warn).toHaveBeenCalled();
    warn.mockRestore();
  });

  it("lets direct pipeline runtime parameters override pipelineConfig defaults", async () => {
    const ocr = await PaddleOCR.create({
      pipelineConfig: pipelineConfigText,
      text_det_limit_side_len: 200,
      text_rec_score_thresh: 0.42,
      ...IGNORE_UNSUPPORTED
    });

    expect(ocr.pipelineConfig.runtimeDefaults.text_det_limit_side_len).toBe(200);
    expect(ocr.pipelineConfig.runtimeDefaults.text_rec_score_thresh).toBe(0.42);
  });

  it("lets direct batch sizes override pipelineConfig", async () => {
    const ocr = await PaddleOCR.create({
      pipelineConfig: pipelineConfigText,
      textRecognitionBatchSize: 2,
      text_detection_batch_size: 3,
      ...IGNORE_UNSUPPORTED
    });

    expect(ocr.pipelineConfig.textRecognitionBatchSize).toBe(2);
    expect(ocr.pipelineConfig.textDetectionBatchSize).toBe(3);
  });

  it("applies textRecognitionBatchSize without user pipelineConfig", async () => {
    const ocr = await PaddleOCR.create({
      lang: "ch",
      ocrVersion: "PP-OCRv5",
      text_recognition_batch_size: 4,
      ...CREATE_WITHOUT_INIT
    });

    expect(ocr.pipelineConfig.textRecognitionBatchSize).toBe(4);
  });

  it("can turn unsupported pipeline warnings into errors", async () => {
    await expect(
      PaddleOCR.create({
        pipelineConfig: pipelineConfigText,
        ...CREATE_WITHOUT_INIT,
        unsupportedBehavior: "error"
      })
    ).rejects.toThrow(/not yet supported/i);
  });

  it("parses the generated default OCR pipeline config text", () => {
    const parsed = parseOcrPipelineConfigText(DEFAULT_OCR_PIPELINE_CONFIG_TEXT);
    const normalized = normalizeOcrPipelineConfig(DEFAULT_OCR_PIPELINE_CONFIG_TEXT);

    expect(normalized).toMatchObject(normalizeOcrPipelineConfig(parsed));
  });

  it("rejects custom model_dir without matching model_name", async () => {
    await expect(
      PaddleOCR.create({
        textDetectionModelAsset: customDetAsset,
        ...CREATE_WITHOUT_INIT
      })
    ).rejects.toThrow(/text_detection_model_dir requires text_detection_model_name/i);
  });

  it("rejects conflicting model asset aliases", async () => {
    await expect(
      PaddleOCR.create({
        text_detection_model_name: "custom_det",
        textDetectionModelAsset: customDetAsset,
        text_detection_model_dir: customRecAsset,
        ...CREATE_WITHOUT_INIT
      })
    ).rejects.toThrow(/Conflicting values provided for text detection model asset/i);
  });

  it("ignores unsupported asset metadata before inference.yml is loaded", async () => {
    const ocr = await PaddleOCR.create({
      text_detection_model_name: "custom_det",
      textDetectionModelAsset: {
        ...customDetAsset,
        model_name: "other_det"
      },
      text_recognition_model_name: "custom_rec",
      textRecognitionModelAsset: customRecAsset,
      ...CREATE_WITHOUT_INIT
    });

    expect(ocr.options.pipelineConfig.assets.det?.url).toBe("https://example.com/custom-det.tar");
  });

  it("initializes worker mode through the same API surface", async () => {
    const worker = new MockWorker((message) => {
      if (message.type === "init") {
        return {
          kind: "worker-transport-response",
          status: "success",
          requestId: message.requestId,
          payload: {
            summary: {
              backend: "wasm",
              webgpuAvailable: false,
              detProvider: "wasm",
              recProvider: "wasm",
              assets: [],
              elapsedMs: 12,
              pipelineConfigWarnings: []
            },
            modelConfig: {
              det: { resizeLong: 960 },
              rec: { imageShape: [3, 48, 320] }
            }
          }
        };
      }
      if (message.type === "dispose") {
        return {
          kind: "worker-transport-response",
          status: "success",
          requestId: message.requestId,
          payload: {}
        };
      }
      return null;
    });
    const ocr = await PaddleOCR.create({
      worker: {
        createWorker: () => worker
      },
      ortOptions: {
        backend: "wasm",
        proxy: true
      },
      ...CREATE_WITHOUT_INIT
    });

    const summary = await ocr.initialize();

    expect(summary.backend).toBe("wasm");
    expect(worker.messages[0].type).toBe("init");
    expect(worker.messages[0].payload.options.ortOptions.backend).toBe("wasm");
    expect(worker.messages[0].payload.options.ortOptions.disableWasmProxy).toBe(true);
    expect(ocr.getModelConfig().det.resizeLong).toBe(960);

    await ocr.dispose();
    expect(worker.messages[1].type).toBe("dispose");
    expect(worker.terminated).toBe(true);
  });

  it("surfaces worker initialization failures", async () => {
    const worker = new MockWorker((message) => {
      if (message.type === "init") {
        return {
          kind: "worker-transport-response",
          status: "error",
          requestId: message.requestId,
          error: {
            name: "Error",
            message: "worker init failed"
          }
        };
      }
      return null;
    });
    const ocr = await PaddleOCR.create({
      worker: {
        createWorker: () => worker
      },
      ...CREATE_WITHOUT_INIT
    });

    await expect(ocr.initialize()).rejects.toThrow(/worker init failed/i);
    expect(worker.terminated).toBe(true);
  });
});

describe("OCR runtime parameter normalization", () => {
  it("accepts camelCase aliases while preserving PaddleOCR names", () => {
    const params = getOcrRuntimeParams(
      {
        det: {
          resizeLong: 960,
          limitType: "max",
          maxSideLimit: 4000,
          normalize: {
            mean: [0.485, 0.456, 0.406],
            std: [0.229, 0.224, 0.225],
            scale: 1 / 255
          },
          postprocess: {
            thresh: 0.3,
            boxThresh: 0.6,
            unclipRatio: 1.5
          }
        },
        rec: {
          charDict: [],
          imageShape: [3, 48, 320]
        }
      },
      {
        text_det_limit_type: "min"
      },
      {
        textDetThresh: 0.4,
        textDetBoxThresh: 0.7,
        textDetUnclipRatio: 2,
        textRecScoreThresh: 0.2
      }
    );

    expect(params.det.limitType).toBe("min");
    expect(params.det.thresh).toBe(0.4);
    expect(params.det.boxThresh).toBe(0.7);
    expect(params.det.unclipRatio).toBe(2);
    expect(params.pipeline.scoreThresh).toBe(0.2);
  });
});

describe("inference.yml model_name extraction", () => {
  it("prefers Global.model_name when present", () => {
    expect(
      extractInferenceModelName(`
Global:
  model_name: custom_det
PostProcess:
  name: DBPostProcess
`)
    ).toBe("custom_det");
  });

  it("falls back to nested model_name fields", () => {
    expect(
      extractInferenceModelName(`
Deploy:
  metadata:
    model_name: custom_rec
`)
    ).toBe("custom_rec");
  });

  it("returns null when inference.yml has no model_name", () => {
    expect(
      extractInferenceModelName(`
PostProcess:
  name: DBPostProcess
`)
    ).toBeNull();
  });
});
