import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import type { LogRecord } from "./types.js";
import { JsonLinesHttpTransport } from "./json-lines-transport.js";

function record(overrides?: Partial<LogRecord>): LogRecord {
  return {
    ts: "2025-01-01T00:00:00.000Z",
    level: "info",
    msg: "test",
    fields: {},
    ...overrides,
  };
}

describe("JsonLinesHttpTransport", () => {
  let fetchSpy: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    vi.useFakeTimers();
    fetchSpy = vi.fn(async () => new Response(null, { status: 200 }));
    vi.stubGlobal("fetch", fetchSpy);
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.restoreAllMocks();
  });

  it("flushes batch after batchSize records", async () => {
    const transport = new JsonLinesHttpTransport({
      endpoint: "https://logs.example/ingest",
      batchSize: 2,
      flushIntervalMs: 60000,
      maxBufferSize: 100,
      fetchImpl: fetchSpy,
    });

    transport.write(record({ msg: "one" }));
    expect(fetchSpy).not.toHaveBeenCalled();

    transport.write(record({ msg: "two" }));
    // Should have flushed after 2nd record
    expect(fetchSpy).toHaveBeenCalledTimes(1);

    const body = fetchSpy.mock.calls[0]?.[1]?.body as string;
    const lines = body.trim().split("\n");
    expect(lines).toHaveLength(2);
  });

  it("flushes after flushIntervalMs even if batch is not full", async () => {
    const transport = new JsonLinesHttpTransport({
      endpoint: "https://logs.example/ingest",
      batchSize: 100,
      flushIntervalMs: 1000,
      maxBufferSize: 100,
      fetchImpl: fetchSpy,
    });

    transport.write(record());
    expect(fetchSpy).not.toHaveBeenCalled();

    vi.advanceTimersByTime(1001);
    expect(fetchSpy).toHaveBeenCalledTimes(1);
  });

  it("drops oldest records when buffer exceeds maxBufferSize", () => {
    const transport = new JsonLinesHttpTransport({
      endpoint: "https://logs.example/ingest",
      batchSize: 100,
      flushIntervalMs: 60000,
      maxBufferSize: 3,
      fetchImpl: fetchSpy,
    });

    transport.write(record({ msg: "1" }));
    transport.write(record({ msg: "2" }));
    transport.write(record({ msg: "3" }));
    transport.write(record({ msg: "4" }));
    transport.write(record({ msg: "5" }));

    // Force flush to see what's in the buffer
    transport.flush();
    const body = fetchSpy.mock.calls[0]?.[1]?.body as string;
    const lines = body.trim().split("\n");
    expect(lines).toHaveLength(3);
    // Should contain the 3 most recent
    expect(lines[0]).toContain('"3"');
    expect(lines[1]).toContain('"4"');
    expect(lines[2]).toContain('"5"');
  });

  it("redacts sensitive field names", async () => {
    const transport = new JsonLinesHttpTransport({
      endpoint: "https://logs.example/ingest",
      batchSize: 1,
      flushIntervalMs: 60000,
      maxBufferSize: 100,
      redactFields: ["password", "token", "secret"],
      fetchImpl: fetchSpy,
    });

    transport.write(record({
      fields: { password: "hunter2", token: "abc123", safe: "visible" },
    }));

    const body = fetchSpy.mock.calls[0]?.[1]?.body as string;
    const parsed = JSON.parse(body.trim());
    expect(parsed.fields.password).toBe("[REDACTED]");
    expect(parsed.fields.token).toBe("[REDACTED]");
    expect(parsed.fields.safe).toBe("visible");
  });

  it("flush() sends all buffered records and clears the buffer", async () => {
    const transport = new JsonLinesHttpTransport({
      endpoint: "https://logs.example/ingest",
      batchSize: 100,
      flushIntervalMs: 60000,
      maxBufferSize: 100,
      fetchImpl: fetchSpy,
    });

    transport.write(record({ msg: "a" }));
    transport.write(record({ msg: "b" }));
    await transport.flush();
    expect(fetchSpy).toHaveBeenCalledTimes(1);

    // Flush again — nothing to send
    await transport.flush();
    expect(fetchSpy).toHaveBeenCalledTimes(1);
  });
});
