import { describe, expect, it } from "vitest";
import {
  exportListRows,
  MAXIMUM_LIST_RENDER_TOKEN_EVALUATIONS,
  parseListTemplate,
  renderListRows,
} from "./list-template";
import type { RegexMatchResult } from "../model/match";
import { DEFAULT_REGEX_LIMITS } from "../execution/request-limits";

const matches: readonly RegexMatchResult[] = [
  {
    matchNumber: 1,
    value: "alice,42",
    valueStatus: "complete",
    range: { startUtf16: 4, endUtf16: 12 },
    nativeRange: { start: 4, end: 12, unit: "utf16" },
    captures: [
      {
        groupNumber: 1,
        groupName: "name",
        value: "alice",
        status: "participated",
        range: { startUtf16: 4, endUtf16: 9 },
        nativeRange: { start: 4, end: 9, unit: "utf16" },
      },
      {
        groupNumber: 2,
        status: "did-not-participate",
      },
    ],
  },
];

describe("typed list templates", () => {
  it("renders non-executable match, capture and metadata tokens", () => {
    const template = parseListTemplate(
      "{{match}}:${name}:$2:$0:{{line}}:{{source}}",
    );
    const { rows } = renderListRows(
      template,
      matches,
      "one\nalice,42",
      "data.txt",
    );
    expect(rows[0]?.value).toBe("1:alice::alice,42:2:data.txt");
    expect(rows[0]?.captures).toEqual({
      "1": "alice",
      name: "alice",
      "2": null,
    });
    expect(rows[0]?.complete).toBe(true);
  });

  it("uses the participating capture for duplicate names", () => {
    const duplicateNames: readonly RegexMatchResult[] = [
      {
        ...matches[0]!,
        captures: [
          {
            groupNumber: 1,
            groupName: "value",
            value: "selected",
            status: "participated",
            range: { startUtf16: 4, endUtf16: 12 },
            nativeRange: { start: 4, end: 12, unit: "utf16" },
          },
          {
            groupNumber: 2,
            groupName: "value",
            status: "did-not-participate",
          },
        ],
      },
    ];

    const { rows } = renderListRows(
      parseListTemplate("${value}"),
      duplicateNames,
      "one\nselected",
    );
    expect(rows[0]?.value).toBe("selected");
    expect(rows[0]?.captures).toEqual({
      "1": "selected",
      "2": null,
      value: "selected",
    });
  });

  it("supports capture 1000 and Unicode ECMAScript group names", () => {
    const extended: readonly RegexMatchResult[] = [
      {
        ...matches[0]!,
        captures: [
          {
            groupNumber: 1_000,
            value: "last",
            status: "participated",
            range: { startUtf16: 4, endUtf16: 8 },
            nativeRange: { start: 4, end: 8, unit: "utf16" },
          },
          {
            groupNumber: 2,
            groupName: "π",
            value: "three",
            status: "participated",
            range: { startUtf16: 4, endUtf16: 9 },
            nativeRange: { start: 4, end: 9, unit: "utf16" },
          },
        ],
      },
    ];

    const template = parseListTemplate("$1000:${π}");
    expect(template.tokens).toEqual([
      { kind: "numbered-capture", number: 1_000 },
      { kind: "literal", value: ":" },
      { kind: "named-capture", name: "π" },
    ]);
    expect(
      renderListRows(template, extended, "one\nlastthree").rows[0]?.value,
    ).toBe("last:three");
  });

  it("quotes CSV correctly and preserves null capture state in JSON", () => {
    const { rows } = renderListRows(
      parseListTemplate("$0"),
      matches,
      "one\nalice,42",
    );
    expect(exportListRows(rows, "csv")).toContain('"alice,42"');
    expect(
      JSON.parse(exportListRows(rows, "json"))[0].captures["2"],
    ).toBeNull();
    expect(exportListRows(rows, "ndjson").trim().split("\n")).toHaveLength(1);
  });

  it("does not present clipped engine previews as exact export data", () => {
    const exact = "x".repeat(70_000);
    const { rows } = renderListRows(
      parseListTemplate("$0:$1"),
      [
        {
          matchNumber: 1,
          value: exact.slice(0, 64 * 1024),
          valueStatus: "truncated",
          range: { startUtf16: 0, endUtf16: exact.length },
          nativeRange: { start: 0, end: exact.length, unit: "utf16" },
          captures: [
            {
              groupNumber: 1,
              value: exact.slice(0, 64 * 1024),
              status: "truncated",
              range: { startUtf16: 0, endUtf16: exact.length },
              nativeRange: { start: 0, end: exact.length, unit: "utf16" },
            },
          ],
        },
      ],
      exact,
    );
    expect(rows[0]?.value).toBe(":");
    expect(rows[0]?.complete).toBe(false);
    expect(rows[0]?.captures).toEqual({ "1": null });
  });

  it("marks an export incomplete when only a clipped capture preview exists", () => {
    const { rows } = renderListRows(
      parseListTemplate("$1"),
      [
        {
          matchNumber: 1,
          value: "a",
          valueStatus: "complete",
          range: { startUtf16: 0, endUtf16: 1 },
          nativeRange: { start: 0, end: 1, unit: "utf16" },
          captures: [
            {
              groupNumber: 1,
              value: "clipped",
              status: "truncated",
            },
          ],
        },
      ],
      "a",
    );
    expect(rows[0]).toEqual(
      expect.objectContaining({
        value: "",
        complete: false,
        captures: { "1": null },
      }),
    );
    expect(rows[0]?.warnings[0]).toContain("clipped preview was not exported");
  });

  it("bounds each generated row before materializing its output", () => {
    const rendered = renderListRows(
      parseListTemplate("$0".repeat(8_192)),
      [{ ...matches[0]!, value: "alice,420" }],
      "one\nalice,420",
    );
    expect(rendered.rows[0]?.value).toHaveLength(64 * 1024);
    expect(rendered.rows[0]?.complete).toBe(false);
    expect(rendered.rows[0]?.warnings[0]).toContain("preview limit");
  });

  it("rejects oversized list templates without parsing or exporting a prefix", () => {
    const source = "$0".repeat(
      DEFAULT_REGEX_LIMITS.maximumListTemplateUtf16 / 2 + 1,
    );
    const template = parseListTemplate(source);
    const rendered = renderListRows(template, matches, "one\nalice,42");

    expect(template.accepted).toBe(false);
    expect(template.tokens).toEqual([]);
    expect(rendered.templateAccepted).toBe(false);
    expect(rendered.rows).toEqual([]);
    expect(rendered.warnings[0]).toContain("was not parsed or rendered");
  });

  it("stops empty-token list evaluation at an explicit operation bound", () => {
    const template = parseListTemplate("$99".repeat(2_000));
    const requestedRows =
      Math.floor(MAXIMUM_LIST_RENDER_TOKEN_EVALUATIONS / 2_000) + 1;
    const rendered = renderListRows(
      template,
      Array.from({ length: requestedRows }, (_, index) => ({
        ...matches[0]!,
        matchNumber: index + 1,
      })),
      "one\nalice,42",
    );

    expect(template.accepted).toBe(true);
    expect(rendered.truncated).toBe(true);
    expect(rendered.rows).toHaveLength(requestedRows);
    expect(rendered.rows.at(-1)?.complete).toBe(false);
    expect(rendered.warnings[0]).toContain("token-operation limit");
  });

  it("refuses to treat an engine-limited match prefix as complete", () => {
    const rendered = renderListRows(
      parseListTemplate("$0"),
      matches,
      "one\nalice,42",
      "test-text",
      true,
    );

    expect(rendered.rows).toHaveLength(1);
    expect(rendered.truncated).toBe(true);
    expect(rendered.sourceResultTruncated).toBe(true);
    expect(rendered.warnings[0]).toContain("engine stopped collecting matches");
  });
});
