import { describe, expect, it, vi } from "vitest";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import {
  runGrowthAnalysis,
  runRegexBenchmark,
  validateBenchmarkRequest,
  validateGrowthRequest,
} from "./analysis-runner";
import type { AnalysisWorkerClient } from "./AnalysisSupervisor";
import type {
  BenchmarkWorkerSample,
  GrowthWorkerSample,
  RegexBenchmarkRequest,
  GrowthAnalysisRequest,
} from "./analysis.types";

function benchmarkSample(
  milliseconds: number,
  overrides: Partial<BenchmarkWorkerSample> = {},
): BenchmarkWorkerSample {
  return {
    accepted: true,
    effectiveFlags: "dg",
    subjectBytes: 3,
    subjectUtf16: 3,
    compileMs: milliseconds,
    firstMatchMs: milliseconds + 1,
    allMatchesMs: milliseconds + 2,
    replacementMs: milliseconds + 3,
    throughputBytesPerSecond: milliseconds * 100,
    matchCount: 1,
    matched: true,
    matchCollectionTruncated: false,
    replacementOutputUtf16: 3,
    ...overrides,
  };
}

function growthSample(
  executionMs: number,
  overrides: Partial<GrowthWorkerSample> = {},
): GrowthWorkerSample {
  return {
    accepted: true,
    effectiveFlags: "d",
    subjectBytes: 1,
    subjectUtf16: 1,
    executionMs,
    matchCount: 0,
    matched: false,
    matchCollectionTruncated: false,
    ...overrides,
  };
}

function client(
  overrides: Partial<AnalysisWorkerClient> = {},
): AnalysisWorkerClient {
  return {
    identity: vi.fn().mockResolvedValue({
      flavour: "ecmascript",
      engineName: "Native ECMAScript RegExp",
      engineVersion: "Test Browser 1",
      runtimeVersion: "Test Browser 1",
      nativeOffsetUnit: "utf16",
    }),
    benchmarkSample: vi.fn().mockResolvedValue(benchmarkSample(1)),
    growthProbe: vi.fn().mockResolvedValue(growthSample(1)),
    cancel: vi.fn(),
    dispose: vi.fn(),
    ...overrides,
  };
}

function benchmarkRequest(
  overrides: Partial<RegexBenchmarkRequest> = {},
): RegexBenchmarkRequest {
  return {
    flavour: "ecmascript",
    pattern: "a+",
    flags: ["g"],
    subject: "aaa",
    replacement: "x",
    scanAll: true,
    maximumMatches: 10_000,
    settings: {
      warmupIterations: 1,
      measuredIterations: 3,
      sampleTimeoutMs: 100,
      maximumWallTimeMs: 1_000,
    },
    ...overrides,
  };
}

function growthRequest(
  overrides: Partial<GrowthAnalysisRequest> = {},
): GrowthAnalysisRequest {
  return {
    flavour: "ecmascript",
    pattern: "(a+)+$",
    flags: [],
    scanAll: false,
    maximumMatches: 10_000,
    settings: {
      prefix: "",
      repeatedFragment: "a",
      suffix: "!",
      startingRepetitions: 16,
      maximumRepetitions: 64,
      multiplier: 2,
      maximumSteps: 3,
      sampleTimeoutMs: 100,
      maximumWallTimeMs: 1_000,
      maximumSubjectBytes: 1_024,
      normalizedGrowthThreshold: 4,
    },
    ...overrides,
  };
}

describe("bounded analysis runner", () => {
  it("keeps cold use, warm-up and measured statistics separate", async () => {
    const samples = [
      benchmarkSample(9),
      benchmarkSample(99),
      benchmarkSample(1),
      benchmarkSample(2),
      benchmarkSample(10),
    ];
    const worker = client({
      benchmarkSample: vi
        .fn()
        .mockImplementation(() => Promise.resolve(samples.shift()!)),
    });
    const phases: string[] = [];

    const result = await runRegexBenchmark(benchmarkRequest(), worker, {
      onProgress: (value) => phases.push(value.phase),
    });

    expect(result.status).toBe("complete");
    expect(result.coldSample?.compileMs).toBe(9);
    expect(result.completedWarmups).toBe(1);
    expect(result.completedMeasuredIterations).toBe(3);
    expect(result.warmStatistics.compileMs).toEqual({
      count: 3,
      minimum: 1,
      median: 2,
      p95: 10,
      maximum: 10,
    });
    expect(result.identity?.engineVersion).toBe("Test Browser 1");
    expect(phases).toEqual([
      "starting-worker",
      "cold-sample",
      "warming-up",
      "measuring",
      "measuring",
      "measuring",
    ]);
  });

  it("returns partial measured samples and a distinct timeout state", async () => {
    let calls = 0;
    const worker = client({
      benchmarkSample: vi.fn().mockImplementation(() => {
        calls += 1;
        if (calls === 3) {
          return Promise.reject(
            new WorkerRequestError("timeout", "sample timed out"),
          );
        }
        return Promise.resolve(benchmarkSample(calls));
      }),
    });

    const result = await runRegexBenchmark(
      benchmarkRequest({
        settings: {
          warmupIterations: 0,
          measuredIterations: 3,
          sampleTimeoutMs: 100,
          maximumWallTimeMs: 1_000,
        },
      }),
      worker,
    );

    expect(result.status).toBe("timeout");
    expect(result.completedMeasuredIterations).toBe(1);
    expect(result.stoppedReason).toContain("worker was terminated");
  });

  it("does not mislabel an aggregate wall limit as a regex timeout", async () => {
    let time = 0;
    const clock = { now: () => time };
    const worker = client({
      identity: vi.fn().mockImplementation(async () => {
        time = 950;
        return {
          flavour: "ecmascript",
          engineName: "Native ECMAScript RegExp",
          engineVersion: "Test Browser 1",
          runtimeVersion: "Test Browser 1",
          nativeOffsetUnit: "utf16",
        } as const;
      }),
      benchmarkSample: vi
        .fn()
        .mockRejectedValue(new WorkerRequestError("timeout", "wall stop")),
    });

    const result = await runRegexBenchmark(
      benchmarkRequest({
        settings: {
          warmupIterations: 0,
          measuredIterations: 1,
          sampleTimeoutMs: 100,
          maximumWallTimeMs: 1_000,
        },
      }),
      worker,
      {},
      clock,
    );

    expect(result.status).toBe("wall-time-limit");
    expect(result.stoppedReason).toContain("aggregate wall-time");
  });

  it("rejects out-of-bound benchmark and growth settings before worker use", () => {
    expect(() =>
      validateBenchmarkRequest(
        benchmarkRequest({
          settings: {
            warmupIterations: 101,
            measuredIterations: 1,
            sampleTimeoutMs: 100,
            maximumWallTimeMs: 1_000,
          },
        }),
      ),
    ).toThrow(/Warm-up iterations/u);
    expect(() =>
      validateGrowthRequest(
        growthRequest({
          settings: {
            ...growthRequest().settings,
            repeatedFragment: "",
          },
        }),
      ),
    ).toThrow(/must not be empty/u);
  });

  it("stops on observed normalized growth and records dynamic evidence", async () => {
    const probes = [growthSample(0.5), growthSample(10)];
    const worker = client({
      growthProbe: vi
        .fn()
        .mockImplementation(() => Promise.resolve(probes.shift()!)),
    });

    const result = await runGrowthAnalysis(growthRequest(), worker);

    expect(result.status).toBe("complete");
    expect(result.stopReason).toBe("growth-threshold");
    expect(result.samples).toHaveLength(2);
    expect(result.samples[1]?.normalizedGrowth).toBeGreaterThan(4);
    expect(result.dynamicFindings[0]).toEqual(
      expect.objectContaining({
        rule: "observed-growth",
        evidence: "dynamically-observed",
        confidence: "medium",
      }),
    );
  });

  it("distinguishes an observed timeout from a crash", async () => {
    const worker = client({
      growthProbe: vi
        .fn()
        .mockRejectedValue(
          new WorkerRequestError("timeout", "probe timed out"),
        ),
    });

    const result = await runGrowthAnalysis(growthRequest(), worker);

    expect(result.status).toBe("timeout");
    expect(result.stopReason).toBe("timeout");
    expect(result.samples[0]?.status).toBe("timeout");
    expect(result.dynamicFindings[0]?.rule).toBe("observed-timeout");
  });

  it("reports a worker crash without relabelling it as a timeout", async () => {
    const worker = client({
      growthProbe: vi
        .fn()
        .mockRejectedValue(new WorkerRequestError("crash", "fixture crash")),
    });

    const result = await runGrowthAnalysis(growthRequest(), worker);

    expect(result.status).toBe("error");
    expect(result.stopReason).toBe("crash");
    expect(result.samples[0]?.status).toBe("crash");
    expect(result.dynamicFindings).toEqual([]);
    expect(result.stoppedReason).toContain("fixture crash");
  });

  it("preflights generated bytes and honours cancellation without allocating a subject", async () => {
    const worker = client();
    const tooLarge = await runGrowthAnalysis(
      growthRequest({
        settings: {
          ...growthRequest().settings,
          prefix: "prefix",
          maximumSubjectBytes: 8,
        },
      }),
      worker,
    );
    expect(tooLarge.stopReason).toBe("maximum-subject-size");
    expect(worker.growthProbe).not.toHaveBeenCalled();

    const controller = new AbortController();
    controller.abort();
    const cancelled = await runGrowthAnalysis(growthRequest(), client(), {
      signal: controller.signal,
    });
    expect(cancelled.status).toBe("cancelled");
    expect(cancelled.stopReason).toBe("cancelled");
  });
});
