import { describe, expect, it, vi } from "vitest";
import {
  executeEcmaScript,
  replaceEcmaScript,
} from "../execution/adapters/ecmascript/EcmaScriptEngineAdapter";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../model/match";
import type { RegexSyntaxRequest } from "../model/syntax";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import { EcmaScriptSyntaxProvider } from "../syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
import type { RegexTestCase } from "../tests/test-case.types";
import {
  PatternFormatValidator,
  type PatternFormatEngineRunner,
  type PatternFormatSyntaxRunner,
} from "./PatternFormatValidator";
import type { PatternFormatValidationInput } from "./formatting.types";

const provider = new EcmaScriptSyntaxProvider();

function syntaxRunner(): PatternFormatSyntaxRunner {
  return {
    parsePattern: (request: RegexSyntaxRequest) =>
      provider.parsePattern(request),
    cancel: vi.fn(),
    dispose: vi.fn(),
  };
}

function engineRunner(
  mapPattern: (pattern: string) => string = (pattern) => pattern,
): PatternFormatEngineRunner {
  return {
    execute: vi.fn(async (request: RegexExecutionRequest) =>
      executeEcmaScript({
        ...request,
        pattern: mapPattern(request.pattern),
      }),
    ),
    replace: vi.fn(async (request: RegexReplacementRequest) =>
      replaceEcmaScript({
        ...request,
        pattern: mapPattern(request.pattern),
      }),
    ),
    cancel: vi.fn(),
    dispose: vi.fn(),
  };
}

function testCase(overrides: Partial<RegexTestCase> = {}): RegexTestCase {
  return {
    id: "slash-count",
    name: "Two slash paths",
    enabled: true,
    flavour: "ecmascript",
    pattern: "a/b",
    flags: ["g", "u"],
    options: {},
    scanAll: true,
    subject: "a/b a/b",
    expectation: { kind: "match-count", count: 2 },
    ...overrides,
  };
}

function input(
  overrides: Partial<PatternFormatValidationInput> = {},
): PatternFormatValidationInput {
  return {
    flavour: "ecmascript",
    sourcePattern: "a/b",
    formattedPattern: "a\\/b",
    flags: ["g", "u"],
    options: {},
    subject: "a/b a/b",
    replacement: "[$&]",
    scanAll: true,
    timeoutMs: 2_000,
    tests: [
      testCase(),
      testCase({
        id: "other-pattern",
        name: "Independent fixture",
        pattern: "other",
      }),
    ],
    ...overrides,
  };
}

function validator(
  formattedPatternMap: (pattern: string) => string = (pattern) => pattern,
) {
  return new PatternFormatValidator({
    createSyntaxRunner: () => syntaxRunner(),
    createEngineRunner: (label) =>
      engineRunner(label === "formatted" ? formattedPatternMap : undefined),
    now: () => performance.now(),
  });
}

describe("PatternFormatValidator", () => {
  it("reparses, recompiles and compares current replacement plus exact applicable tests", async () => {
    const instance = validator();
    const progress = vi.fn();

    const result = await instance.validate(input(), progress);

    expect(result.status).toBe("equivalent");
    expect(result.canApply).toBe(true);
    expect(result.current.comparison).toBe("same");
    expect(result.current.before.engineIdentity).toContain(
      "Native ECMAScript RegExp",
    );
    expect(result.applicableTestCount).toBe(1);
    expect(result.skippedTestCount).toBe(1);
    expect(result.completedTestCount).toBe(1);
    expect(result.tests[0]).toEqual(
      expect.objectContaining({
        testId: "slash-count",
        comparison: "same",
      }),
    );
    expect(result.tests[0]?.before.assertionPassed).toBe(true);
    expect(result.tests[0]?.after.assertionPassed).toBe(true);
    expect(result.differences).toEqual([]);
    expect(progress).toHaveBeenLastCalledWith(1, 1);
    instance.dispose();
  });

  it("blocks application and reports exact semantic differences", async () => {
    const instance = validator((pattern) =>
      pattern === "a\\/b" ? "z+" : pattern,
    );

    const result = await instance.validate(input());

    expect(result.status).toBe("different");
    expect(result.canApply).toBe(false);
    expect(result.current.comparison).toBe("different");
    expect(result.current.mismatchKinds).toContain("match-count");
    expect(result.differences).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          scope: "current-subject",
          code: "current-behavior-changed",
        }),
        expect.objectContaining({
          scope: "unit-test",
          code: "test-behavior-changed",
          testId: "slash-count",
        }),
      ]),
    );
    instance.dispose();
  });

  it("does not execute a formatted pattern rejected by the syntax provider", async () => {
    const sourceEngine = engineRunner();
    const formattedEngine = engineRunner();
    const instance = new PatternFormatValidator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: (label) =>
        label === "source" ? sourceEngine : formattedEngine,
      now: () => performance.now(),
    });

    const result = await instance.validate(input({ formattedPattern: "(" }));

    expect(result.status).toBe("different");
    expect(result.formattedSyntaxAccepted).toBe(false);
    expect(result.canApply).toBe(false);
    expect(formattedEngine.replace).not.toHaveBeenCalled();
    expect(result.differences).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ code: "formatted-syntax-rejected" }),
      ]),
    );
    instance.dispose();
  });

  it("reports no applicable tests explicitly without weakening the current snapshot gate", async () => {
    const instance = validator();
    const result = await instance.validate(
      input({
        tests: [
          testCase({ enabled: false }),
          testCase({ id: "other", pattern: "other" }),
        ],
      }),
    );

    expect(result.status).toBe("equivalent");
    expect(result.applicableTestCount).toBe(0);
    expect(result.notices.join(" ")).toContain(
      "No enabled unit test has the exact active pattern",
    );
    instance.dispose();
  });

  it("blocks before execution when reparsing changes the capture shape", async () => {
    const sourceEngine = engineRunner();
    const formattedEngine = engineRunner();
    const instance = new PatternFormatValidator({
      createSyntaxRunner: (label) => {
        const runner = syntaxRunner();
        return label === "source"
          ? runner
          : {
              ...runner,
              parsePattern: async (request: RegexSyntaxRequest) => {
                const result = await provider.parsePattern(request);
                return {
                  ...result,
                  captures: [
                    {
                      number: 1,
                      name: "invented",
                      range: { startUtf16: 0, endUtf16: 1 },
                      repeated: false,
                    },
                  ],
                };
              },
            };
      },
      createEngineRunner: (label) =>
        label === "source" ? sourceEngine : formattedEngine,
      now: () => performance.now(),
    });

    const result = await instance.validate(input());

    expect(result.status).toBe("different");
    expect(result.captureShapePreserved).toBe(false);
    expect(result.differences).toEqual(
      expect.arrayContaining([
        expect.objectContaining({ code: "capture-shape-changed" }),
      ]),
    );
    expect(sourceEngine.replace).not.toHaveBeenCalled();
    expect(formattedEngine.replace).not.toHaveBeenCalled();
    instance.dispose();
  });

  it("treats engine-identity drift as inconclusive", async () => {
    const instance = new PatternFormatValidator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: (label) => {
        const runner = engineRunner();
        if (label === "source") return runner;
        return {
          ...runner,
          replace: vi.fn(async (request: RegexReplacementRequest) => {
            const replacement = await replaceEcmaScript(request);
            return {
              ...replacement,
              execution: {
                ...replacement.execution,
                engine: {
                  ...replacement.execution.engine,
                  engineVersion: "different-runtime",
                },
              },
            };
          }),
        };
      },
      now: () => performance.now(),
    });

    const result = await instance.validate(input({ tests: [] }));

    expect(result.status).toBe("inconclusive");
    expect(result.canApply).toBe(false);
    expect(result.current.comparison).toBe("inconclusive");
    expect(result.current.mismatchKinds).toEqual(["engine-identity"]);
    instance.dispose();
  });

  it("treats truncated semantic output as inconclusive", async () => {
    const instance = new PatternFormatValidator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: (label) => {
        const runner = engineRunner();
        if (label === "source") return runner;
        return {
          ...runner,
          replace: vi.fn(async (request: RegexReplacementRequest) => {
            const replacement = await replaceEcmaScript(request);
            return {
              ...replacement,
              execution: {
                ...replacement.execution,
                truncated: true,
              },
              truncated: true,
            };
          }),
        };
      },
      now: () => performance.now(),
    });

    const result = await instance.validate(input({ tests: [] }));

    expect(result.status).toBe("inconclusive");
    expect(result.canApply).toBe(false);
    expect(result.current.comparison).toBe("inconclusive");
    expect(result.current.summary).toContain("incomplete data");
    instance.dispose();
  });

  it("accepts matching fixed unit-test timeouts but not two current-snapshot timeouts", async () => {
    const timedTest = testCase({
      subject: "slow",
      expectation: { kind: "must-time-out" },
    });
    const createRunner = (): PatternFormatEngineRunner => {
      const runner = engineRunner();
      return {
        ...runner,
        execute: vi.fn(async (request: RegexExecutionRequest) => {
          if (request.subject === "slow") {
            throw new WorkerRequestError("timeout", "Fixture timeout.");
          }
          return executeEcmaScript(request);
        }),
      };
    };
    const instance = new PatternFormatValidator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: () => createRunner(),
      now: () => performance.now(),
    });

    const result = await instance.validate(input({ tests: [timedTest] }));

    expect(result.status).toBe("equivalent");
    expect(result.tests[0]).toEqual(
      expect.objectContaining({
        comparison: "same",
        before: expect.objectContaining({
          status: "timeout",
          assertionPassed: true,
        }),
        after: expect.objectContaining({
          status: "timeout",
          assertionPassed: true,
        }),
      }),
    );
    instance.dispose();

    const currentTimeout = new PatternFormatValidator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: () => ({
        ...engineRunner(),
        replace: vi.fn(async () => {
          throw new WorkerRequestError("timeout", "Fixture timeout.");
        }),
      }),
      now: () => performance.now(),
    });
    const currentResult = await currentTimeout.validate(input({ tests: [] }));
    expect(currentResult.status).toBe("inconclusive");
    expect(currentResult.current.comparison).toBe("inconclusive");
    currentTimeout.dispose();
  });

  it("does not start a test whose full timeout no longer fits the aggregate wall budget", async () => {
    let clock = 0;
    const sourceEngine = engineRunner();
    const formattedEngine = engineRunner();
    const sourceReplace = sourceEngine.replace.bind(sourceEngine);
    const formattedReplace = formattedEngine.replace.bind(formattedEngine);
    sourceEngine.replace = vi.fn(async (request, timeoutMs) => {
      const result = await sourceReplace(request, timeoutMs);
      clock = 59_000;
      return result;
    });
    formattedEngine.replace = vi.fn(async (request, timeoutMs) => {
      const result = await formattedReplace(request, timeoutMs);
      clock = 59_000;
      return result;
    });
    const instance = new PatternFormatValidator({
      createSyntaxRunner: () => syntaxRunner(),
      createEngineRunner: (label) =>
        label === "source" ? sourceEngine : formattedEngine,
      now: () => clock,
    });

    const result = await instance.validate(
      input({
        tests: [
          testCase({
            resourceLimits: { manualExecutionTimeoutMs: 2_000 },
          }),
        ],
      }),
    );

    expect(result.status).toBe("inconclusive");
    expect(result.applicableTestCount).toBe(1);
    expect(result.completedTestCount).toBe(0);
    expect(sourceEngine.execute).not.toHaveBeenCalled();
    expect(formattedEngine.execute).not.toHaveBeenCalled();
    expect(result.notices.join(" ")).toContain(
      "full 2,000 ms worker timeout no longer fit",
    );
    instance.dispose();
  });
});
