import { describe, expect, it, vi } from "vitest";
import type { EngineSupervisor } from "../execution/EngineSupervisor";
import { DEFAULT_REGEX_LIMITS } from "../execution/request-limits";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import type {
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexReplacementRequest,
} from "../model/match";
import type { CorpusDocument } from "./corpus.types";
import { runCorpusBatch, type CorpusRunConfiguration } from "./corpus-runner";

function document(id: string, text: string): CorpusDocument {
  return {
    id,
    name: `${id}.txt`,
    text,
    bytes: text.length,
    source: "file",
  };
}

function result(
  request: RegexExecutionRequest,
  matchCount: number,
  truncated = false,
): RegexExecutionResult {
  return {
    accepted: true,
    engine: {
      flavour: request.flavour,
      adapterVersion: "test",
      engineName: "Test engine",
      engineVersion: "1",
      runtimeVersion: "1",
      offsetUnit: "utf16",
      capabilities: {
        compilation: true,
        matching: true,
        replacement: true,
        namedCaptures: true,
        captureHistory: false,
        actualTrace: false,
        benchmark: false,
      },
    },
    flags: {
      userFlags: request.flags.join(""),
      effectiveFlags: request.flags.join(""),
      internallyAddedIndicesFlag: false,
      internallyAddedGlobalFlag: false,
    },
    matches: Array.from({ length: matchCount }, (_, index) => ({
      matchNumber: index + 1,
      value: "x",
      valueStatus: "complete" as const,
      range: { startUtf16: index, endUtf16: index + 1 },
      nativeRange: { start: index, end: index + 1, unit: "utf16" as const },
      captures: [],
    })),
    diagnostics: [],
    elapsedMs: 3,
    truncated,
  };
}

const configuration: CorpusRunConfiguration = {
  flavour: "ecmascript",
  flavourVersion: "2025",
  options: {},
  pattern: "x",
  flags: [],
  captureMetadata: [],
  replacement: "y",
  operation: "match",
  subjectMode: "document",
  timeoutMs: 1_000,
};

describe("corpus runner", () => {
  it("processes documents sequentially with exhaustive matching and progress", async () => {
    const requests: RegexExecutionRequest[] = [];
    const engine = {
      execute: vi.fn((request: RegexExecutionRequest) => {
        requests.push(request);
        return Promise.resolve(result(request, request.subject.length));
      }),
      replace: vi.fn(),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;
    const progress: string[] = [];

    const batch = await runCorpusBatch(
      [document("one", "xx"), document("two", "x")],
      configuration,
      {
        engine,
        signal: new AbortController().signal,
        onProgress: (value) =>
          progress.push(
            `${value.completedDocuments}/${value.totalDocuments}:${value.totalMatches}`,
          ),
      },
    );

    expect(batch).toMatchObject({
      status: "complete",
      processedDocuments: 2,
      totalMatches: 3,
    });
    expect(requests).toHaveLength(2);
    expect(requests.every((request) => request.scanAll)).toBe(true);
    expect(progress).toEqual(["0/2:0", "1/2:2", "2/2:3"]);
  });

  it("retains exact applied outputs and marks truncated output partial", async () => {
    const engine = {
      execute: vi.fn(),
      replace: vi.fn((request: RegexReplacementRequest) => {
        const execution = result(request, 1, request.subject === "partial");
        return Promise.resolve({
          execution,
          output: request.subject === "partial" ? "p" : "done",
          outputBytes: request.subject === "partial" ? 1 : 4,
          outputTruncated: request.subject === "partial",
          truncated: request.subject === "partial",
        });
      }),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;

    const batch = await runCorpusBatch(
      [document("one", "ok"), document("two", "partial")],
      { ...configuration, operation: "replace" },
      { engine, signal: new AbortController().signal },
    );

    expect(batch.status).toBe("partial");
    expect(batch.documents[0]).toMatchObject({
      status: "complete",
      output: "done",
      outputBytes: 4,
    });
    expect(batch.documents[1]).toMatchObject({
      status: "partial",
      output: "p",
    });
  });

  it("continues after a document timeout and labels cancellation", async () => {
    const controller = new AbortController();
    const engine = {
      execute: vi
        .fn()
        .mockRejectedValueOnce(
          new WorkerRequestError("timeout", "test engine timed out"),
        )
        .mockImplementationOnce((request: RegexExecutionRequest) => {
          controller.abort();
          return Promise.resolve(result(request, 1));
        }),
      replace: vi.fn(),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;

    const batch = await runCorpusBatch(
      [
        document("timeout", "x"),
        document("complete", "x"),
        document("cancelled", "x"),
      ],
      configuration,
      { engine, signal: controller.signal },
    );

    expect(batch.status).toBe("cancelled");
    expect(batch.documents.map((entry) => entry.status)).toEqual([
      "error",
      "complete",
      "cancelled",
    ]);
    expect(batch.documents[0]?.message).toMatch(/worker was terminated/iu);
  });

  it("distinguishes the aggregate wall-time stop from user cancellation", async () => {
    const controller = new AbortController();
    controller.abort("corpus-wall-time");
    const engine = {
      execute: vi.fn(),
      replace: vi.fn(),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;

    const batch = await runCorpusBatch(
      [document("pending", "x")],
      configuration,
      { engine, signal: controller.signal },
    );

    expect(batch.status).toBe("partial");
    expect(batch.message).toMatch(/wall-time limit/iu);
    expect(batch.documents[0]).toMatchObject({ status: "not-run" });
  });

  it("runs independent lines separately and preserves original separators during apply", async () => {
    const subjects: string[] = [];
    const engine = {
      execute: vi.fn(),
      replace: vi.fn((request: RegexReplacementRequest) => {
        subjects.push(request.subject);
        const execution = result(request, request.subject === "" ? 0 : 1);
        const output = request.subject.toLocaleUpperCase("en-US");
        return Promise.resolve({
          execution,
          output,
          outputBytes: new TextEncoder().encode(output).byteLength,
          outputTruncated: false,
          truncated: false,
        });
      }),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;

    const batch = await runCorpusBatch(
      [document("mixed-lines", "one\r\ntwo\n")],
      {
        ...configuration,
        operation: "replace",
        subjectMode: "line",
      },
      { engine, signal: new AbortController().signal },
    );

    expect(subjects).toEqual(["one", "two", ""]);
    expect(batch.documents[0]).toMatchObject({
      status: "complete",
      lineCount: 3,
      output: "ONE\r\nTWO\n",
      changed: true,
      matchCount: 2,
    });
  });

  it("aggregates bounded full-match and capture extraction summaries", async () => {
    const engine = {
      execute: vi.fn((request: RegexExecutionRequest) => {
        const base = result(request, 0);
        return Promise.resolve({
          ...base,
          matches: [
            {
              matchNumber: 1,
              value: "42",
              valueStatus: "complete" as const,
              range: { startUtf16: 0, endUtf16: 2 },
              nativeRange: {
                start: 0,
                end: 2,
                unit: "utf16" as const,
              },
              captures: [
                {
                  groupNumber: 1,
                  groupName: "number",
                  value: "42",
                  status: "participated" as const,
                  range: { startUtf16: 0, endUtf16: 2 },
                  nativeRange: {
                    start: 0,
                    end: 2,
                    unit: "utf16" as const,
                  },
                },
                {
                  groupNumber: 2,
                  status: "did-not-participate" as const,
                },
              ],
            },
          ],
        });
      }),
      replace: vi.fn(),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;

    const batch = await runCorpusBatch(
      [document("captures", "42")],
      configuration,
      { engine, signal: new AbortController().signal },
    );

    expect(batch.documents[0]?.captureSummaries).toEqual([
      expect.objectContaining({
        groupNumber: 0,
        participated: 1,
        samples: ["42"],
      }),
      expect.objectContaining({
        groupNumber: 1,
        groupName: "number",
        participated: 1,
        samples: ["42"],
      }),
      expect.objectContaining({
        groupNumber: 2,
        didNotParticipate: 1,
        samples: [],
      }),
    ]);
  });

  it("preflights document and independent-line limits", async () => {
    const engine = {
      execute: vi.fn(),
      replace: vi.fn(),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;
    const dependencies = {
      engine,
      signal: new AbortController().signal,
    };

    await expect(
      runCorpusBatch(
        Array.from(
          { length: DEFAULT_REGEX_LIMITS.maximumCorpusDocuments + 1 },
          (_, index) => document(`document-${index}`, ""),
        ),
        configuration,
        dependencies,
      ),
    ).rejects.toThrow(/document limit/iu);

    await expect(
      runCorpusBatch(
        [
          document(
            "too-many-lines",
            "\n".repeat(DEFAULT_REGEX_LIMITS.maximumCorpusLines),
          ),
        ],
        { ...configuration, subjectMode: "line" },
        dependencies,
      ),
    ).rejects.toThrow(/logical lines/iu);
    expect(engine.execute).not.toHaveBeenCalled();
  });

  it("caps retained capture groups and sample values explicitly", async () => {
    const oversizedValue =
      "x".repeat(DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSampleUtf16) + "tail";
    const engine = {
      execute: vi.fn((request: RegexExecutionRequest) => {
        const base = result(request, 0);
        return Promise.resolve({
          ...base,
          matches: [
            {
              matchNumber: 1,
              value: oversizedValue,
              valueStatus: "complete" as const,
              range: { startUtf16: 0, endUtf16: oversizedValue.length },
              nativeRange: {
                start: 0,
                end: oversizedValue.length,
                unit: "utf16" as const,
              },
              captures: Array.from(
                {
                  length:
                    DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSummaries + 5,
                },
                (_, index) => ({
                  groupNumber: index + 1,
                  value: String(index),
                  status: "participated" as const,
                }),
              ),
            },
          ],
        });
      }),
      replace: vi.fn(),
      cancel: vi.fn(),
    } as unknown as EngineSupervisor;

    const batch = await runCorpusBatch(
      [document("many-captures", oversizedValue)],
      configuration,
      { engine, signal: new AbortController().signal },
    );

    expect(batch.documents[0]?.captureSummaries).toHaveLength(
      DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSummaries,
    );
    expect(batch.documents[0]?.captureSummariesTruncated).toBe(true);
    expect(batch.documents[0]?.captureSummaries[0]).toMatchObject({
      groupNumber: 0,
      samplesClipped: true,
    });
    expect(batch.documents[0]?.captureSummaries[0]?.samples[0]).toHaveLength(
      DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSampleUtf16,
    );
  });
});
