/**
 * Generic URL parity test harness.
 *
 * Features register their URL serializer/parser against this harness.
 * The harness runs:
 *   1. Table-driven fixture tests (parse + build parity against corpus).
 *   2. fast-check property-based fuzz tests (roundtrip: parse(build(x)) === x).
 *
 * Designed for reuse across Phase 3+ features.
 *
 * @module
 */

import { describe, it, expect } from "vitest";
import * as fc from "fast-check";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

// ---------------------------------------------------------------------------
// Public config contract
// ---------------------------------------------------------------------------

export interface UrlParityConfig<TQuery> {
  /** Feature name (used in describe block labels). */
  feature: string;

  /** Path to the JSON fixture file relative to the project root. */
  fixturePath: string;

  /** Parse a raw URL string into the typed query object. Returns null on failure. */
  parse(raw: string): TQuery | null;

  /** Build a URL string from a typed query object. */
  build(query: TQuery): string;

  /** fast-check arbitrary for generating random valid query objects. */
  fuzzArbitrary: fc.Arbitrary<TQuery>;
}

// ---------------------------------------------------------------------------
// Fixture shape
// ---------------------------------------------------------------------------

interface FixtureEntry<T> {
  url: string;
  expected: T;
}

// ---------------------------------------------------------------------------
// Harness entry point
// ---------------------------------------------------------------------------

/**
 * Defines a full URL parity test suite for a feature.
 *
 * Call this from a `*.test.ts` file — it registers `describe` / `it` blocks
 * via Vitest globals.
 */
export function defineUrlParityTests<T>(config: UrlParityConfig<T>): void {
  const { feature, fixturePath, parse, build, fuzzArbitrary } = config;

  // -------------------------------------------------------------------------
  // Load fixtures
  // -------------------------------------------------------------------------

  const fixtureAbsPath = resolve(process.cwd(), fixturePath);
  const raw = readFileSync(fixtureAbsPath, "utf-8");
  const fixtures = JSON.parse(raw) as FixtureEntry<T>[];

  // -------------------------------------------------------------------------
  // 1. Table-driven fixture tests
  // -------------------------------------------------------------------------

  describe(`[${feature}] URL parity — fixture corpus (${fixtures.length} entries)`, () => {
    for (const entry of fixtures) {
      it(`parse("${entry.url}") matches expected`, () => {
        const parsed = parse(entry.url);
        expect(parsed).toEqual(entry.expected);
      });

      it(`build(expected) === "${entry.url}"`, () => {
        const built = build(entry.expected);
        expect(built).toBe(entry.url);
      });

      it(`roundtrip: build(parse("${entry.url}")) === "${entry.url}"`, () => {
        const parsed = parse(entry.url);
        expect(parsed).not.toBeNull();
        if (parsed === null) return;
        const rebuilt = build(parsed);
        expect(rebuilt).toBe(entry.url);
      });
    }
  });

  // -------------------------------------------------------------------------
  // 2. fast-check fuzz roundtrip tests
  // -------------------------------------------------------------------------

  describe(`[${feature}] URL parity — fast-check fuzz roundtrip`, () => {
    it("parse(build(params)) deep-equals params for all generated inputs", () => {
      fc.assert(
        fc.property(fuzzArbitrary, (params) => {
          const url = build(params);
          const parsed = parse(url);
          expect(parsed).toEqual(params);
        }),
        { numRuns: 200 },
      );
    });

    it("build(parse(build(params))) === build(params) for all generated inputs", () => {
      fc.assert(
        fc.property(fuzzArbitrary, (params) => {
          const url = build(params);
          const parsed = parse(url);
          expect(parsed).not.toBeNull();
          if (parsed === null) return;
          const rebuilt = build(parsed);
          expect(rebuilt).toBe(url);
        }),
        { numRuns: 200 },
      );
    });
  });
}
