import expect = require("expect.js");
import { io } from "..";
import { wrap, BASE_URL, success } from "./support/util";

describe("socket", () => {
  it("should have an accessible socket id equal to the server-side socket id (default namespace)", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, { forceNew: true });

      socket.emit("getId", (id) => {
        expect(socket.id).to.be.ok();
        expect(socket.id).to.be.eql(id);
        expect(socket.id).to.not.eql(socket.io.engine.id);
        socket.disconnect();
        done();
      });
    });
  });

  it("should have an accessible socket id equal to the server-side socket id (custom namespace)", () => {
    return wrap((done) => {
      const socket = io(BASE_URL + "/foo", { forceNew: true });
      socket.emit("getId", (id) => {
        expect(socket.id).to.be.ok();
        expect(socket.id).to.be.eql(id);
        expect(socket.id).to.not.eql(socket.io.engine.id);
        socket.disconnect();
        done();
      });
    });
  });

  it("clears socket.id upon disconnection", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, { forceNew: true });
      socket.on("connect", () => {
        socket.on("disconnect", () => {
          expect(socket.id).to.not.be.ok();
          done();
        });

        socket.disconnect();
      });
    });
  });

  it("doesn't fire an error event if we force disconnect in opening state", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, { forceNew: true, timeout: 100 });
      socket.disconnect();
      socket.io.on("error", () => {
        throw new Error("Unexpected");
      });
      setTimeout(() => {
        done();
      }, 300);
    });
  });

  it("fire a connect_error event when the connection cannot be established", () => {
    return wrap((done) => {
      const socket = io("http://localhost:9823", {
        forceNew: true,
        timeout: 100,
      });
      socket.on("connect_error", () => {
        socket.close();
        done();
      });
    });
  });

  it("fire a connect_error event on open timeout (polling)", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, {
        forceNew: true,
        transports: ["polling"],
        timeout: 0,
      });

      socket.on("connect_error", (err) => {
        expect(err.message).to.eql("timeout");
        socket.disconnect();
        done();
      });
    });
  });

  it("fire a connect_error event on open timeout (websocket)", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, {
        forceNew: true,
        transports: ["websocket"],
        timeout: 0,
      });

      socket.on("connect_error", (err) => {
        expect(err.message).to.eql("timeout");
        socket.disconnect();
        done();
      });
    });
  });

  it("doesn't fire a connect_error event when the connection is already established", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, { forceNew: true });
      socket.on("connect", () => {
        socket.io.engine.close();
      });
      socket.on("connect_error", () => {
        done(new Error("should not happen"));
      });
      setTimeout(() => {
        socket.close();
        done();
      }, 300);
    });
  });

  it("should change socket.id upon reconnection", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, { forceNew: true, reconnectionDelay: 10 });
      socket.on("connect", () => {
        const id = socket.id;

        socket.io.on("reconnect_attempt", () => {
          expect(socket.id).to.not.be.ok();
        });

        socket.io.on("reconnect", () => {
          expect(socket.id).to.not.eql(id);
          socket.disconnect();
          done();
        });

        socket.io.engine.close();
      });
    });
  });

  it("should enable compression by default", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, { forceNew: true });
      socket.on("connect", () => {
        socket.io.engine.once("packetCreate", (packet) => {
          expect(packet.options.compress).to.be(true);
          socket.disconnect();
          done();
        });
        socket.emit("hi");
      });
    });
  });

  it("should disable compression", () => {
    return wrap((done) => {
      const socket = io(BASE_URL, { forceNew: true });
      socket.on("connect", () => {
        socket.io.engine.once("packetCreate", (packet) => {
          expect(packet.options.compress).to.be(false);
          socket.disconnect();
          done();
        });
        socket.compress(false).emit("hi");
      });
    });
  });

  describe("query option", () => {
    it("should accept an object (default namespace)", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/", {
          forceNew: true,
          query: { e: "f" },
        });

        socket.emit("getHandshake", (handshake) => {
          expect(handshake.query.e).to.be("f");
          socket.disconnect();
          done();
        });
      });
    });

    it("should accept a query string (default namespace)", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/?c=d", { forceNew: true });

        socket.emit("getHandshake", (handshake) => {
          expect(handshake.query.c).to.be("d");
          socket.disconnect();
          done();
        });
      });
    });

    it("should accept an object", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
          query: { a: "b" },
        });

        socket.on("handshake", (handshake) => {
          expect(handshake.query.a).to.be("b");
          socket.disconnect();
          done();
        });
      });
    });

    it("should accept a query string", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc?b=c&d=e", { forceNew: true });

        socket.on("handshake", (handshake) => {
          expect(handshake.query.b).to.be("c");
          expect(handshake.query.d).to.be("e");
          socket.disconnect();
          done();
        });
      });
    });

    it("should properly encode the parameters", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
          query: { "&a": "&=?a" },
        });

        socket.on("handshake", (handshake) => {
          expect(handshake.query["&a"]).to.be("&=?a");
          socket.disconnect();
          done();
        });
      });
    });
  });

  describe("auth option", () => {
    it("should accept an object", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
          auth: { a: "b", c: "d" },
        });

        socket.on("handshake", (handshake) => {
          expect(handshake.auth.a).to.be("b");
          expect(handshake.auth.c).to.be("d");
          expect(handshake.query.a).to.be(undefined);
          socket.disconnect();
          done();
        });
      });
    });

    it("should accept an function", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
          auth: (cb) => cb({ e: "f" }),
        });

        socket.on("handshake", (handshake) => {
          expect(handshake.auth.e).to.be("f");
          expect(handshake.query.e).to.be(undefined);
          socket.disconnect();
          done();
        });
      });
    });
  });

  it("should fire an error event on middleware failure from custom namespace", () => {
    return wrap((done) => {
      const socket = io(BASE_URL + "/no", { forceNew: true });
      socket.on("connect_error", (err) => {
        expect(err).to.be.an(Error);
        expect(err.message).to.eql("Auth failed (custom namespace)");
        socket.disconnect();
        done();
      });
    });
  });

  it("should not try to reconnect after a middleware failure", () => {
    return wrap((done) => {
      const socket = io(BASE_URL + "/no", {
        forceNew: true,
        reconnectionDelay: 10,
      });

      let count = 0;

      socket.on("connect_error", () => {
        count++;
        // force reconnection
        socket.io.engine.close();
      });

      setTimeout(() => {
        expect(count).to.eql(1);
        done();
      }, 100);
    });
  });

  it("should properly disconnect then reconnect", () => {
    return wrap((done) => {
      const socket = io(BASE_URL + "/", {
        forceNew: true,
        transports: ["websocket"],
      });

      let count = 0;

      socket.once("connect", () => {
        socket.disconnect().connect();
      });

      socket.on("disconnect", () => {
        count++;
      });

      setTimeout(() => {
        expect(count).to.eql(1);
        success(done, socket);
      }, 100);
    });
  });

  it("should throw on reserved event", () => {
    const socket = io(BASE_URL + "/no", { forceNew: true });

    expect(() => socket.emit("disconnecting", "goodbye")).to.throwException(
      /"disconnecting" is a reserved event name/,
    );
  });

  it("should emit events in order", () => {
    return wrap((done) => {
      const socket = io(BASE_URL + "/", { autoConnect: false });
      let i = 0;

      socket.on("connect", () => {
        socket.emit("echo", "second", () => {
          expect(++i).to.eql(2);

          socket.disconnect();
          done();
        });
      });

      socket.emit("echo", "first", () => {
        expect(++i).to.eql(1);
      });

      socket.connect();
    });
  });

  it("should emit an event and wait for the acknowledgement", () => {
    return wrap(async (done) => {
      const socket = io(BASE_URL, { forceNew: true });

      const val = await socket.emitWithAck("echo", 123);
      expect(val).to.be(123);

      success(done, socket);
    });
  });

  describe("volatile packets", () => {
    it("should discard a volatile packet when the socket is not connected", () => {
      return wrap((done) => {
        const socket = io(BASE_URL, { forceNew: true, autoConnect: false });

        socket.volatile.emit("getId", () => {
          done(new Error("should not happen"));
        });

        socket.emit("getId", () => {
          socket.disconnect();
          done();
        });

        socket.connect();
      });
    });

    it("should discard a volatile packet when the pipe is not ready", () => {
      return wrap((done) => {
        const socket = io(BASE_URL, { forceNew: true });

        socket.on("connect", () => {
          socket.emit("getId", () => {
            socket.disconnect();
            done();
          });

          socket.volatile.emit("getId", () => {
            done(new Error("should not happen"));
          });
        });
      });
    });

    it("should send a volatile packet when the socket is connected and the pipe is ready", () => {
      return wrap((done) => {
        const socket = io(BASE_URL, { forceNew: true });

        const interval = setInterval(() => {
          socket.volatile.emit("getId", () => {
            clearInterval(interval);
            socket.disconnect();
            done();
          });
        }, 200);
      });
    });
  });

  describe("onAny", () => {
    it("should call listener", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
        });

        socket.onAny((event, arg1) => {
          expect(event).to.be("handshake");
          expect(arg1).to.be.an(Object);

          success(done, socket);
        });
      });
    });

    it("should prepend listener", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", { forceNew: true });

        let count = 0;

        socket.onAny((event, arg1) => {
          expect(count).to.be(2);

          success(done, socket);
        });

        socket.prependAny(() => {
          expect(count++).to.be(1);
        });

        socket.prependAny(() => {
          expect(count++).to.be(0);
        });
      });
    });

    it("should remove listener", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
        });

        let count = 0;

        const fail = () => done(new Error("fail"));

        socket.onAny(fail);
        socket.offAny(fail);
        expect(socket.listenersAny.length).to.be(0);

        socket.onAny(() => {
          success(done, socket);
        });
      });
    });
  });

  describe("onAnyOutgoing", () => {
    it("should call listener", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
        });

        socket.on("connect", () => {
          socket.onAnyOutgoing((event, arg1) => {
            expect(event).to.be("my-event");
            expect(arg1).to.be("123");

            success(done, socket);
          });

          socket.emit("my-event", "123");
        });
      });
    });

    it("should call listener with binary data", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
        });

        socket.on("connect", () => {
          socket.onAnyOutgoing((event, arg1) => {
            expect(event).to.be("my-event");
            expect(arg1).to.be.an(Uint8Array);

            success(done, socket);
          });

          socket.emit("my-event", Uint8Array.of(1, 2, 3));
        });
      });
    });

    it("should prepend listener", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
        });

        let count = 0;

        socket.onAnyOutgoing(() => {
          expect(count).to.be(2);

          success(done, socket);
        });

        socket.prependAnyOutgoing(() => {
          expect(count++).to.be(1);
        });

        socket.prependAnyOutgoing(() => {
          expect(count++).to.be(0);
        });

        socket.emit("my-event", "123");
      });
    });

    it("should remove listener", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/abc", {
          forceNew: true,
        });
        const fail = () => done(new Error("fail"));

        socket.onAnyOutgoing(fail);
        socket.offAnyOutgoing(fail);
        expect(socket.listenersAnyOutgoing.length).to.be(0);

        socket.onAnyOutgoing(() => {
          success(done, socket);
        });

        socket.emit("my-event", "123");
      });
    });
  });

  describe("timeout", () => {
    it("should timeout after the given delay when socket is not connected", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/", {
          autoConnect: false,
        });

        socket.timeout(50).emit("event", (err) => {
          expect(err).to.be.an(Error);
          expect(socket.sendBuffer).to.be.empty();

          success(done, socket);
        });
      });
    });

    it("should timeout when the server does not acknowledge the event", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/");

        socket.timeout(50).emit("unknown", (err) => {
          expect(err).to.be.an(Error);
          success(done, socket);
        });
      });
    });

    it("should timeout when the server does not acknowledge the event in time", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/");

        let count = 0;

        socket.timeout(0).emit("echo", 42, (err) => {
          expect(err).to.be.an(Error);
          count++;
        });

        setTimeout(() => {
          expect(count).to.eql(1);
          success(done, socket);
        }, 200);
      });
    });

    it("should not timeout when the server does acknowledge the event", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/");

        socket.timeout(50).emit("echo", 42, (err, value) => {
          expect(err).to.be(null);
          expect(value).to.be(42);
          success(done, socket);
        });
      });
    });

    it("should timeout when the server does not acknowledge the event (promise)", () => {
      return wrap(async (done) => {
        const socket = io(BASE_URL + "/");

        try {
          await socket.timeout(50).emitWithAck("unknown");
          done(new Error("should not happen"));
        } catch (e) {
          success(done, socket);
        }
      });
    });

    it("should not timeout when the server does acknowledge the event (promise)", () => {
      return wrap(async (done) => {
        const socket = io(BASE_URL + "/");

        try {
          const value = await socket.timeout(50).emitWithAck("echo", 42);
          expect(value).to.be(42);
          success(done, socket);
        } catch (e) {
          done(new Error("should not happen"));
        }
      });
    });

    it("should use the default timeout value", () => {
      return wrap((done) => {
        const socket = io(BASE_URL + "/", {
          ackTimeout: 50,
        });

        socket.emit("unknown", (err) => {
          expect(err).to.be.an(Error);
          success(done, socket);
        });
      });
    });

    describe("acknowledgement upon disconnection", () => {
      it("should not ack upon disconnection (callback)", () => {
        return wrap((done) => {
          const socket = io(BASE_URL, {
            forceNew: true,
          });

          socket.on("connect", () => {
            socket.emit("echo", "a", (_value) => {
              done(new Error("should not happen"));
            });

            socket.disconnect();

            // @ts-ignore property 'acks' is private
            expect(Object.keys(socket.acks).length).to.eql(0);

            setTimeout(() => success(done, socket), 100);
          });
        });
      });

      it("should ack with an error upon disconnection (callback & timeout)", () => {
        return wrap((done) => {
          const socket = io(BASE_URL, {
            forceNew: true,
          });

          socket.on("connect", () => {
            socket.timeout(10_000).emit("echo", "a", (err) => {
              expect(err).to.be.an(Error);

              // @ts-ignore property 'acks' is private
              expect(Object.keys(socket.acks).length).to.eql(0);

              success(done, socket);
            });

            socket.disconnect();
          });
        });
      });

      it("should ack with an error upon disconnection (callback & ackTimeout)", () => {
        return wrap((done) => {
          const socket = io(BASE_URL, {
            forceNew: true,
            ackTimeout: 10_000,
          });

          socket.on("connect", () => {
            socket.emit("echo", "a", (err) => {
              expect(err).to.be.an(Error);

              success(done, socket);
            });

            socket.disconnect();
          });
        });
      });

      it("should ack with an error upon disconnection (promise)", () => {
        return wrap((done) => {
          const socket = io(BASE_URL, {
            forceNew: true,
          });

          socket.on("connect", () => {
            socket.emitWithAck("echo", "a").catch((err) => {
              expect(err).to.be.an(Error);

              success(done, socket);
            });

            socket.disconnect();
          });
        });
      });

      it("should ack with an error upon disconnection (promise & timeout)", () => {
        return wrap((done) => {
          const socket = io(BASE_URL, {
            forceNew: true,
          });

          socket.on("connect", () => {
            socket
              .timeout(10_000)
              .emitWithAck("echo", "a")
              .catch((err) => {
                expect(err).to.be.an(Error);

                success(done, socket);
              });

            socket.disconnect();
          });
        });
      });

      it("should not discard an unsent ack (callback)", () => {
        return wrap((done) => {
          const socket = io(BASE_URL, {
            forceNew: true,
          });

          socket.once("connect", () => {
            socket.disconnect();

            // the packet will be buffered and sent upon reconnection
            socket.emit("echo", "a", (value) => {
              expect(value).to.eql("a");

              success(done, socket);
            });

            setTimeout(() => socket.connect(), 100);
          });
        });
      });
    });
  });

  describe("throttled timer", () => {
    it("should buffer the event and send it upon reconnection", () => {
      return wrap((done) => {
        let hasReconnected = false;

        const socket = io(BASE_URL, {
          forceNew: true,
          reconnectionDelay: 10,
        });

        socket.once("connect", () => {
          // @ts-expect-error simulate a throttled timer
          socket.io.engine._pingTimeoutTime = Date.now() - 1;

          socket.emit("echo", "123", (value) => {
            expect(hasReconnected).to.be(true);
            expect(value).to.eql("123");

            success(done, socket);
          });
        });

        socket.io.once("reconnect", () => {
          hasReconnected = true;
        });
      });
    });
  });
});
