import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { loadFixture, parseFixture, saveFixture, serializeFixture } from "./fixture.js";
import type { HttpExchangeFixture } from "./types.js";

const fixture: HttpExchangeFixture = {
  request: {
    method: "POST",
    url: "https://api.example.com/v1/chat",
    headers: { "content-type": "application/json" },
    body: '{"model":"gpt-4"}',
  },
  response: {
    status: 200,
    statusText: "OK",
    headers: { "content-type": "application/json" },
    body: '{"choices":[]}',
  },
  meta: { model: "gpt-4", capturedAt: 1700000000000, label: null },
};

const minimalFixture: HttpExchangeFixture = {
  request: { method: "GET", url: "https://x", headers: {}, body: null },
  response: { status: 204, headers: {}, body: "" },
};

describe("serializeFixture", () => {
  it("produces valid JSON", () => {
    const json = serializeFixture(fixture);
    expect(() => JSON.parse(json)).not.toThrow();
  });

  it("round-trips through parseFixture", () => {
    const json = serializeFixture(fixture);
    const parsed = parseFixture(json);
    expect(parsed).toEqual(fixture);
  });

  it("round-trips minimal fixture", () => {
    const json = serializeFixture(minimalFixture);
    const parsed = parseFixture(json);
    expect(parsed).toEqual(minimalFixture);
  });

  it("produces pretty-printed JSON with trailing newline", () => {
    const json = serializeFixture(fixture);
    expect(json).toContain("\n");
    expect(json.endsWith("\n")).toBe(true);
  });
});

describe("parseFixture", () => {
  it("throws on invalid JSON", () => {
    expect(() => parseFixture("not json")).toThrow("Invalid JSON");
  });

  it("throws on non-object", () => {
    expect(() => parseFixture('"hello"')).toThrow("Fixture must be an object");
  });

  it("throws on null", () => {
    expect(() => parseFixture("null")).toThrow("Fixture must be an object");
  });

  it("throws on missing request", () => {
    expect(() => parseFixture('{"response":{"status":200,"headers":{},"body":""}}')).toThrow(
      "request",
    );
  });

  it("throws on missing response", () => {
    expect(() =>
      parseFixture('{"request":{"method":"GET","url":"x","headers":{},"body":null}}'),
    ).toThrow("response");
  });

  it("throws on non-string request.method", () => {
    const bad = {
      request: { method: 123, url: "x", headers: {}, body: null },
      response: { status: 200, headers: {}, body: "" },
    };
    expect(() => parseFixture(JSON.stringify(bad))).toThrow("request.method must be a string");
  });

  it("throws on non-number response.status", () => {
    const bad = {
      request: { method: "GET", url: "x", headers: {}, body: null },
      response: { status: "200", headers: {}, body: "" },
    };
    expect(() => parseFixture(JSON.stringify(bad))).toThrow("response.status must be a number");
  });

  it("throws on non-string response.body", () => {
    const bad = {
      request: { method: "GET", url: "x", headers: {}, body: null },
      response: { status: 200, headers: {}, body: 123 },
    };
    expect(() => parseFixture(JSON.stringify(bad))).toThrow("response.body must be a string");
  });

  it("throws on non-string header value", () => {
    const bad = {
      request: { method: "GET", url: "x", headers: { bad: 123 }, body: null },
      response: { status: 200, headers: {}, body: "" },
    };
    expect(() => parseFixture(JSON.stringify(bad))).toThrow("headers.bad must be a string");
  });

  it("throws on invalid meta value", () => {
    const bad = {
      request: { method: "GET", url: "x", headers: {}, body: null },
      response: { status: 200, headers: {}, body: "" },
      meta: { bad: {} },
    };
    expect(() => parseFixture(JSON.stringify(bad))).toThrow("meta.bad must be");
  });

  it("accepts valid meta with null/string/number/boolean", () => {
    const withMeta = {
      ...minimalFixture,
      meta: { a: "s", b: 1, c: true, d: null },
    };
    const parsed = parseFixture(JSON.stringify(withMeta));
    expect(parsed.meta).toEqual({ a: "s", b: 1, c: true, d: null });
  });

  it("accepts fixture without meta", () => {
    const parsed = parseFixture(JSON.stringify(minimalFixture));
    expect(parsed.meta).toBeUndefined();
  });
});

let tmpDir: string;

beforeEach(async () => {
  tmpDir = await mkdtemp(join(tmpdir(), "trace-replay-test-"));
});

afterEach(async () => {
  await rm(tmpDir, { recursive: true, force: true });
});

describe("saveFixture / loadFixture", () => {
  it("writes and reads back a fixture (real fs)", () => {
    const path = join(tmpDir, "test-fixture.json");
    saveFixture(path, fixture);
    const loaded = loadFixture(path);
    expect(loaded).toEqual(fixture);
  });

  it("writes and reads back a minimal fixture", () => {
    const path = join(tmpDir, "minimal.json");
    saveFixture(path, minimalFixture);
    const loaded = loadFixture(path);
    expect(loaded).toEqual(minimalFixture);
  });

  it("loadFixture throws on malformed file", () => {
    const path = join(tmpDir, "bad.json");
    const { writeFileSync } = require("node:fs") as typeof import("node:fs");
    writeFileSync(path, "not json", "utf-8");
    expect(() => loadFixture(path)).toThrow("Invalid JSON");
  });
});
