import test from "node:test";
import assert from "node:assert/strict";
import * as net from "node:net";
import { EventEmitter } from "node:events";
import {
  getMpvReconnectDelay,
  MpvSocketMessagePayload,
  MpvSocketTransport,
  scheduleMpvReconnect,
} from "./mpv-transport";

class FakeSocket extends EventEmitter {
  public connectedPaths: string[] = [];
  public writePayloads: string[] = [];
  public destroyed = false;

  connect(path: string): void {
    this.connectedPaths.push(path);
    setTimeout(() => {
      this.emit("connect");
    }, 0);
  }

  write(payload: string): boolean {
    this.writePayloads.push(payload);
    return true;
  }

  destroy(): void {
    this.destroyed = true;
    this.emit("close");
  }
}

function withSocketMock<T>(fn: () => T): T {
  const OriginalSocket = net.Socket;
  (net as any).Socket = FakeSocket as any;
  try {
    return fn();
  } finally {
    (net as any).Socket = OriginalSocket;
  }
}

const wait = () => new Promise((resolve) => setTimeout(resolve, 0));

test("getMpvReconnectDelay follows existing reconnect ramp", () => {
  assert.equal(getMpvReconnectDelay(0, true), 1000);
  assert.equal(getMpvReconnectDelay(1, true), 1000);
  assert.equal(getMpvReconnectDelay(2, true), 2000);
  assert.equal(getMpvReconnectDelay(4, true), 5000);
  assert.equal(getMpvReconnectDelay(7, true), 10000);

  assert.equal(getMpvReconnectDelay(0, false), 200);
  assert.equal(getMpvReconnectDelay(2, false), 500);
  assert.equal(getMpvReconnectDelay(4, false), 1000);
  assert.equal(getMpvReconnectDelay(6, false), 2000);
});

test("scheduleMpvReconnect clears existing timer and increments attempt", () => {
  const existing = {} as ReturnType<typeof setTimeout>;
  const cleared: Array<ReturnType<typeof setTimeout> | null> = [];
  const setTimers: Array<ReturnType<typeof setTimeout> | null> = [];
  const calls: Array<{ attempt: number; delay: number }> = [];
  let connected = 0;

  const originalSetTimeout = globalThis.setTimeout;
  const originalClearTimeout = globalThis.clearTimeout;
  (globalThis as any).setTimeout = (handler: () => void, _delay: number) => {
    handler();
    return 1 as unknown as ReturnType<typeof setTimeout>;
  };
  (globalThis as any).clearTimeout = (timer: ReturnType<typeof setTimeout> | null) => {
    cleared.push(timer);
  };

  const nextAttempt = scheduleMpvReconnect({
    attempt: 3,
    hasConnectedOnce: true,
    getReconnectTimer: () => existing,
    setReconnectTimer: (timer) => {
      setTimers.push(timer);
    },
    onReconnectAttempt: (attempt, delay) => {
      calls.push({ attempt, delay });
    },
    connect: () => {
      connected += 1;
    },
  });

  (globalThis as any).setTimeout = originalSetTimeout;
  (globalThis as any).clearTimeout = originalClearTimeout;

  assert.equal(nextAttempt, 4);
  assert.equal(cleared.length, 1);
  assert.equal(cleared[0], existing);
  assert.equal(setTimers.length, 1);
  assert.equal(calls.length, 1);
  assert.equal(calls[0].attempt, 4);
  assert.equal(calls[0].delay, getMpvReconnectDelay(3, true));
  assert.equal(connected, 1);
});

test("MpvSocketTransport connects and sends payloads over a live socket", async () => {
  const events: string[] = [];
  await withSocketMock(async () => {
    const transport = new MpvSocketTransport({
      socketPath: "/tmp/mpv.sock",
      onConnect: () => {
        events.push("connect");
      },
      onData: () => {
        events.push("data");
      },
      onError: () => {
        events.push("error");
      },
      onClose: () => {
        events.push("close");
      },
    });

    const payload: MpvSocketMessagePayload = {
      command: ["sub-seek", 1],
      request_id: 1,
    };

    assert.equal(transport.send(payload), false);

    transport.connect();
    await wait();

    assert.equal(events.includes("connect"), true);
    assert.equal(transport.send(payload), true);

    const fakeSocket = transport.getSocket() as unknown as FakeSocket;
    assert.equal(fakeSocket.connectedPaths.at(0), "/tmp/mpv.sock");
    assert.equal(fakeSocket.writePayloads.length, 1);
    assert.equal(fakeSocket.writePayloads.at(0), `${JSON.stringify(payload)}\n`);
  });
});

test("MpvSocketTransport reports lifecycle transitions and callback order", async () => {
  const events: string[] = [];
  const fakeError = new Error("boom");

  await withSocketMock(async () => {
    const transport = new MpvSocketTransport({
      socketPath: "/tmp/mpv.sock",
      onConnect: () => {
        events.push("connect");
      },
      onData: () => {
        events.push("data");
      },
      onError: () => {
        events.push("error");
      },
      onClose: () => {
        events.push("close");
      },
    });

    transport.connect();
    await wait();

    const socket = transport.getSocket() as unknown as FakeSocket;
    socket.emit("error", fakeError);
    socket.emit("data", Buffer.from("{}"));
    socket.destroy();
    await wait();

    assert.equal(events.includes("connect"), true);
    assert.equal(events.includes("data"), true);
    assert.equal(events.includes("error"), true);
    assert.equal(events.includes("close"), true);
    assert.equal(transport.isConnected, false);
    assert.equal(transport.isConnecting, false);
    assert.equal(socket.destroyed, true);
  });
});

test("MpvSocketTransport ignores connect requests while already connecting or connected", async () => {
  const events: string[] = [];

  await withSocketMock(async () => {
    const transport = new MpvSocketTransport({
      socketPath: "/tmp/mpv.sock",
      onConnect: () => {
        events.push("connect");
      },
      onData: () => {
        events.push("data");
      },
      onError: () => {
        events.push("error");
      },
      onClose: () => {
        events.push("close");
      },
    });

    transport.connect();
    transport.connect();
    await wait();

    assert.equal(events.includes("connect"), true);
    const socket = transport.getSocket() as unknown as FakeSocket;
    socket.emit("close");
    await wait();

    transport.connect();
    await wait();

    assert.equal(events.filter((entry) => entry === "connect").length, 2);
  });
});

test("MpvSocketTransport.shutdown clears socket and lifecycle flags", async () => {
  await withSocketMock(async () => {
    const transport = new MpvSocketTransport({
      socketPath: "/tmp/mpv.sock",
      onConnect: () => {
      },
      onData: () => {
      },
      onError: () => {
      },
      onClose: () => {
      },
    });

    transport.connect();
    await wait();
    assert.equal(transport.isConnected, true);

    transport.shutdown();
    assert.equal(transport.isConnected, false);
    assert.equal(transport.isConnecting, false);
    assert.equal(transport.getSocket(), null);
  });
});
