import type { SocketId } from "socket.io-adapter";
import { Server, Namespace, Socket } from "..";
import expect from "expect.js";
import {
  success,
  createClient,
  successFn,
  createPartialDone,
  assert,
} from "./support/util";

describe("namespaces", () => {
  it("should be accessible through .sockets", () => {
    const io = new Server();
    expect(io.sockets).to.be.a(Namespace);
  });

  it("should be aliased", () => {
    const io = new Server();
    expect(io.use).to.be.a("function");
    expect(io.to).to.be.a("function");
    expect(io["in"]).to.be.a("function");
    expect(io.emit).to.be.a("function");
    expect(io.send).to.be.a("function");
    expect(io.write).to.be.a("function");
    expect(io.allSockets).to.be.a("function");
    expect(io.compress).to.be.a("function");
  });

  it("should return an immutable broadcast operator", () => {
    const io = new Server();
    const operator = io.local.to(["room1", "room2"]).except("room3");
    operator.compress(true).emit("hello");
    operator.volatile.emit("hello");
    operator.to("room4").emit("hello");
    operator.except("room5").emit("hello");
    io.to("room6").emit("hello");
    // @ts-ignore
    expect(operator.rooms).to.contain("room1", "room2");
    // @ts-ignore
    expect(operator.exceptRooms).to.contain("room3");
    // @ts-ignore
    expect(operator.flags).to.eql({ local: true });
  });

  it("should automatically connect", (done) => {
    const io = new Server(0);
    const socket = createClient(io);
    socket.on("connect", successFn(done, io, socket));
  });

  it("should fire a `connection` event", (done) => {
    const io = new Server(0);
    const clientSocket = createClient(io);

    io.on("connection", (socket) => {
      expect(socket).to.be.a(Socket);
      success(done, io, clientSocket);
    });
  });

  it("should fire a `connect` event", (done) => {
    const io = new Server(0);
    const clientSocket = createClient(io);

    io.on("connection", (socket) => {
      expect(socket).to.be.a(Socket);
      success(done, io, clientSocket);
    });
  });

  it("should work with many sockets", (done) => {
    const io = new Server(0);
    io.of("/chat");
    io.of("/news");
    const chat = createClient(io, "/chat");
    const news = createClient(io, "/news");

    let total = 2;
    chat.on("connect", () => {
      --total || success(done, io, chat, news);
    });
    news.on("connect", () => {
      --total || success(done, io, chat, news);
    });
  });

  it('should be able to equivalently start with "" or "/" on server', (done) => {
    const io = new Server(0);
    const c1 = createClient(io, "/");
    const c2 = createClient(io, "/abc");

    let total = 2;
    io.of("").on("connection", () => {
      --total || success(done, io, c1, c2);
    });
    io.of("abc").on("connection", () => {
      --total || success(done, io, c1, c2);
    });
  });

  it('should be equivalent for "" and "/" on client', (done) => {
    const io = new Server(0);
    const c1 = createClient(io, "");

    io.of("/").on("connection", successFn(done, io, c1));
  });

  it("should work with `of` and many sockets", (done) => {
    const io = new Server(0);
    const chat = createClient(io, "/chat");
    const news = createClient(io, "/news");

    let total = 2;
    io.of("/news").on("connection", (socket) => {
      expect(socket).to.be.a(Socket);
      --total || success(done, io, chat, news);
    });
    io.of("/news").on("connection", (socket) => {
      expect(socket).to.be.a(Socket);
      --total || success(done, io, chat, news);
    });
  });

  it("should work with `of` second param", (done) => {
    const io = new Server(0);
    const chat = createClient(io, "/chat");
    const news = createClient(io, "/news");

    let total = 2;
    io.of("/news", (socket) => {
      expect(socket).to.be.a(Socket);
      --total || success(done, io, chat, news);
    });
    io.of("/news", (socket) => {
      expect(socket).to.be.a(Socket);
      --total || success(done, io, chat, news);
    });
  });

  it("should disconnect upon transport disconnection", (done) => {
    const io = new Server(0);
    const chat = createClient(io, "/chat");
    const news = createClient(io, "/news");

    let total = 2;
    let totald = 2;
    let s;
    io.of("/news", (socket) => {
      socket.on("disconnect", (reason) => {
        --totald || success(done, io, chat, news);
      });
      --total || close();
    });
    io.of("/chat", (socket) => {
      s = socket;
      socket.on("disconnect", (reason) => {
        --totald || success(done, io, chat, news);
      });
      --total || close();
    });
    function close() {
      s.disconnect(true);
    }
  });

  it("should fire a `disconnecting` event just before leaving all rooms", (done) => {
    const io = new Server(0);
    const socket = createClient(io);

    io.on("connection", (s) => {
      s.join("a");
      // FIXME not sure why process.nextTick() is needed here
      process.nextTick(() => s.disconnect());

      let total = 2;
      s.on("disconnecting", (reason) => {
        expect(s.rooms).to.contain(s.id, "a");
        total--;
      });

      s.on("disconnect", (reason) => {
        expect(s.rooms.size).to.eql(0);
        --total || success(done, io, socket);
      });
    });
  });

  it("should return error connecting to non-existent namespace", (done) => {
    const io = new Server(0);
    const socket = createClient(io, "/doesnotexist");

    socket.on("connect_error", (err) => {
      expect(err.message).to.be("Invalid namespace");
      success(done, io);
    });
  });

  it("should not reuse same-namespace connections", (done) => {
    const io = new Server(0);
    const clientSocket1 = createClient(io);
    const clientSocket2 = createClient(io);

    let connections = 0;
    io.on("connection", () => {
      connections++;
      if (connections === 2) {
        success(done, io, clientSocket1, clientSocket2);
      }
    });
  });

  it("should find all clients in a namespace", (done) => {
    const io = new Server(0);
    const chatSids: string[] = [];
    let otherSid: SocketId | null = null;

    const c1 = createClient(io, "/chat");
    const c2 = createClient(io, "/chat", { forceNew: true });
    const c3 = createClient(io, "/other", { forceNew: true });

    let total = 3;
    io.of("/chat").on("connection", (socket) => {
      chatSids.push(socket.id);
      --total || getSockets();
    });
    io.of("/other").on("connection", (socket) => {
      otherSid = socket.id;
      --total || getSockets();
    });

    async function getSockets() {
      const sids = await io.of("/chat").allSockets();

      expect(sids).to.contain(chatSids[0], chatSids[1]);
      expect(sids).to.not.contain(otherSid);
      success(done, io, c1, c2, c3);
    }
  });

  it("should find all clients in a namespace room", (done) => {
    const io = new Server(0);
    let chatFooSid: SocketId | null = null;
    let chatBarSid: SocketId | null = null;
    let otherSid: SocketId | null = null;

    const c1 = createClient(io, "/chat");
    const c2 = createClient(io, "/chat", { forceNew: true });
    const c3 = createClient(io, "/other", { forceNew: true });

    let chatIndex = 0;
    let total = 3;
    io.of("/chat").on("connection", (socket) => {
      if (chatIndex++) {
        socket.join("foo");
        chatFooSid = socket.id;
        --total || getSockets();
      } else {
        socket.join("bar");
        chatBarSid = socket.id;
        --total || getSockets();
      }
    });
    io.of("/other").on("connection", (socket) => {
      socket.join("foo");
      otherSid = socket.id;
      --total || getSockets();
    });

    async function getSockets() {
      const sids = await io.of("/chat").in("foo").allSockets();

      expect(sids).to.contain(chatFooSid);
      expect(sids).to.not.contain(chatBarSid);
      expect(sids).to.not.contain(otherSid);
      success(done, io, c1, c2, c3);
    }
  });

  it("should find all clients across namespace rooms", (done) => {
    const io = new Server(0);
    let chatFooSid: SocketId | null = null;
    let chatBarSid: SocketId | null = null;
    let otherSid: SocketId | null = null;

    const c1 = createClient(io, "/chat");
    const c2 = createClient(io, "/chat", { forceNew: true });
    const c3 = createClient(io, "/other", { forceNew: true });

    let chatIndex = 0;
    let total = 3;
    io.of("/chat").on("connection", (socket) => {
      if (chatIndex++) {
        socket.join("foo");
        chatFooSid = socket.id;
        --total || getSockets();
      } else {
        socket.join("bar");
        chatBarSid = socket.id;
        --total || getSockets();
      }
    });
    io.of("/other").on("connection", (socket) => {
      socket.join("foo");
      otherSid = socket.id;
      --total || getSockets();
    });

    async function getSockets() {
      const sids = await io.of("/chat").allSockets();
      expect(sids).to.contain(chatFooSid, chatBarSid);
      expect(sids).to.not.contain(otherSid);
      success(done, io, c1, c2, c3);
    }
  });

  it("should not emit volatile event after regular event", (done) => {
    const io = new Server(0);

    let counter = 0;
    io.of("/chat").on("connection", (s) => {
      // Wait to make sure there are no packets being sent for opening the connection
      setTimeout(() => {
        io.of("/chat").emit("ev", "data");
        io.of("/chat").volatile.emit("ev", "data");
      }, 50);
    });

    const socket = createClient(io, "/chat");
    socket.on("ev", () => {
      counter++;
    });

    setTimeout(() => {
      expect(counter).to.be(1);
      success(done, io, socket);
    }, 500);
  });

  it("should emit volatile event", (done) => {
    const io = new Server(0);

    let counter = 0;
    io.of("/chat").on("connection", (s) => {
      // Wait to make sure there are no packets being sent for opening the connection
      setTimeout(() => {
        io.of("/chat").volatile.emit("ev", "data");
      }, 100);
    });

    const socket = createClient(io, "/chat");
    socket.on("ev", () => {
      counter++;
    });

    setTimeout(() => {
      expect(counter).to.be(1);
      success(done, io, socket);
    }, 500);
  });

  it("should enable compression by default", (done) => {
    const io = new Server(0);
    const socket = createClient(io, "/chat");

    io.of("/chat").on("connection", (s) => {
      s.conn.once("packetCreate", (packet) => {
        expect(packet.options.compress).to.be(true);
        success(done, io, socket);
      });
      io.of("/chat").emit("woot", "hi");
    });
  });

  it("should disable compression", (done) => {
    const io = new Server(0);
    const socket = createClient(io, "/chat");

    io.of("/chat").on("connection", (s) => {
      s.conn.once("packetCreate", (packet) => {
        expect(packet.options.compress).to.be(false);
        success(done, io, socket);
      });
      io.of("/chat").compress(false).emit("woot", "hi");
    });
  });

  it("should throw on reserved event", () => {
    const io = new Server();

    expect(() => io.emit("connect")).to.throwException(
      /"connect" is a reserved event name/,
    );
  });

  it("should close a client without namespace", (done) => {
    const io = new Server(0, {
      connectTimeout: 10,
    });

    const socket = createClient(io);

    // @ts-ignore
    socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet

    socket.on("disconnect", successFn(done, io, socket));
  });

  it("should exclude a specific socket when emitting", (done) => {
    const io = new Server(0);

    const socket1 = createClient(io, "/");
    const socket2 = createClient(io, "/");

    socket2.on("a", () => {
      done(new Error("should not happen"));
    });
    socket1.on("a", successFn(done, io, socket1, socket2));

    socket2.on("connect", () => {
      assert(socket2.id);
      io.except(socket2.id).emit("a");
    });
  });

  it("should exclude a specific socket when emitting (in a namespace)", (done) => {
    const io = new Server(0);

    const nsp = io.of("/nsp");

    const socket1 = createClient(io, "/nsp");
    const socket2 = createClient(io, "/nsp");

    socket2.on("a", () => {
      done(new Error("not"));
    });
    socket1.on("a", successFn(done, io, socket1, socket2));

    socket2.on("connect", () => {
      assert(socket2.id);
      nsp.except(socket2.id).emit("a");
    });
  });

  it("should exclude a specific room when emitting", (done) => {
    const io = new Server(0);

    const nsp = io.of("/nsp");

    const socket1 = createClient(io, "/nsp");
    const socket2 = createClient(io, "/nsp");

    socket1.on("a", successFn(done, io, socket1, socket2));
    socket2.on("a", () => {
      done(new Error("not"));
    });

    nsp.on("connection", (socket) => {
      socket.on("broadcast", () => {
        socket.join("room1");
        nsp.except("room1").emit("a");
      });
    });

    socket2.emit("broadcast");
  });

  it("should emit an 'new_namespace' event", (done) => {
    const io = new Server();

    io.on("new_namespace", (namespace) => {
      expect(namespace.name).to.eql("/nsp");
      done();
    });

    io.of("/nsp");
  });

  it("should not clean up a non-dynamic namespace", (done) => {
    const io = new Server(0, { cleanupEmptyChildNamespaces: true });
    const c1 = createClient(io, "/chat");

    c1.on("connect", () => {
      c1.disconnect();

      // Give it some time to disconnect the client
      setTimeout(() => {
        expect(io._nsps.has("/chat")).to.be(true);
        expect(io._nsps.get("/chat")!.sockets.size).to.be(0);
        success(done, io);
      }, 100);
    });

    io.of("/chat");
  });

  describe("dynamic namespaces", () => {
    it("should allow connections to dynamic namespaces with a regex", (done) => {
      const io = new Server(0);
      const socket = createClient(io, "/dynamic-101");
      const partialDone = createPartialDone(4, successFn(done, io, socket));

      let dynamicNsp = io
        .of(/^\/dynamic-\d+$/)
        .on("connect", (socket) => {
          expect(socket.nsp.name).to.be("/dynamic-101");
          dynamicNsp.emit("hello", 1, "2", { 3: "4" });
          partialDone();
        })
        .use((socket, next) => {
          next();
          partialDone();
        });
      socket.on("connect_error", (err) => {
        expect().fail();
      });
      socket.on("connect", () => {
        partialDone();
      });
      socket.on("hello", (a, b, c) => {
        expect(a).to.eql(1);
        expect(b).to.eql("2");
        expect(c).to.eql({ 3: "4" });
        partialDone();
      });
    });

    it("should allow connections to dynamic namespaces with a regex and emit in a room", (done) => {
      const io = new Server(0);
      const socket = createClient(io, "/dynamic-101");
      const partialDone = createPartialDone(4, successFn(done, io, socket));

      let dynamicNsp = io
        .of(/^\/dynamic-\d+$/)
        .on("connect", (socket) => {
          expect(socket.nsp.name).to.be("/dynamic-101");
          socket.join("some-room");
          dynamicNsp.to("some-room").emit("hello", 4, "3", { 2: "1" });
          partialDone();
        })
        .use((socket, next) => {
          next();
          partialDone();
        });
      socket.on("connect_error", (err) => {
        expect().fail();
      });
      socket.on("connect", () => {
        partialDone();
      });
      socket.on("hello", (a, b, c) => {
        expect(a).to.eql(4);
        expect(b).to.eql("3");
        expect(c).to.eql({ 2: "1" });
        partialDone();
      });
    });

    it("should allow connections to dynamic namespaces with a function", (done) => {
      const io = new Server(0);
      const socket = createClient(io, "/dynamic-101");

      io.of((name, query, next) => next(null, "/dynamic-101" === name));
      socket.on("connect", successFn(done, io, socket));
    });

    it("should disallow connections when no dynamic namespace matches", (done) => {
      const io = new Server(0);
      const socket = createClient(io, "/abc");
      io.of(/^\/dynamic-\d+$/);
      io.of((name, query, next) => next(null, "/dynamic-101" === name));

      socket.on("connect_error", (err) => {
        expect(err.message).to.be("Invalid namespace");
        success(done, io, socket);
      });
    });

    it("should emit an 'new_namespace' event for a dynamic namespace", (done) => {
      const io = new Server(0);
      io.of(/^\/dynamic-\d+$/);
      const socket = createClient(io, "/dynamic-101");

      io.on("new_namespace", (namespace) => {
        expect(namespace.name).to.be("/dynamic-101");

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

    it("should handle race conditions with dynamic namespaces (#4136)", (done) => {
      const io = new Server(0);
      const counters = {
        connected: 0,
        created: 0,
        events: 0,
      };
      const buffer: Function[] = [];
      io.on("new_namespace", (namespace) => {
        counters.created++;
      });

      const handler = () => {
        if (++counters.events === 2) {
          expect(counters.created).to.equal(1);
          success(done, io, one, two);
        }
      };

      io.of((name, query, next) => {
        buffer.push(next);
        if (buffer.length === 2) {
          buffer.forEach((next) => next(null, true));
        }
      }).on("connection", (socket) => {
        if (++counters.connected === 2) {
          io.of("/dynamic-101").emit("message");
        }
      });

      let one = createClient(io, "/dynamic-101");
      let two = createClient(io, "/dynamic-101");
      one.on("message", handler);
      two.on("message", handler);
    });

    it("should clean up namespace when cleanupEmptyChildNamespaces is on and there are no more sockets in a namespace", (done) => {
      const io = new Server(0, { cleanupEmptyChildNamespaces: true });
      const c1 = createClient(io, "/dynamic-101");

      c1.on("connect", () => {
        c1.disconnect();

        // Give it some time to disconnect and clean up the namespace
        setTimeout(() => {
          expect(io._nsps.has("/dynamic-101")).to.be(false);
          success(done, io);
        }, 100);
      });

      io.of(/^\/dynamic-\d+$/);
    });

    it("should allow a client to connect to a cleaned up namespace", (done) => {
      const io = new Server(0, { cleanupEmptyChildNamespaces: true });
      const c1 = createClient(io, "/dynamic-101");

      c1.on("connect", () => {
        c1.disconnect();

        // Give it some time to disconnect and clean up the namespace
        setTimeout(() => {
          expect(io._nsps.has("/dynamic-101")).to.be(false);

          const c2 = createClient(io, "/dynamic-101");

          c2.on("connect", () => {
            success(done, io, c2);
          });

          c2.on("connect_error", () => {
            done(
              new Error(
                "Client got error when connecting to dynamic namespace",
              ),
            );
          });
        }, 100);
      });

      io.of(/^\/dynamic-\d+$/);
    });

    it("should not clean up namespace when cleanupEmptyChildNamespaces is off and there are no more sockets in a namespace", (done) => {
      const io = new Server(0);
      const c1 = createClient(io, "/dynamic-101");

      c1.on("connect", () => {
        c1.disconnect();

        // Give it some time to disconnect and clean up the namespace
        setTimeout(() => {
          expect(io._nsps.has("/dynamic-101")).to.be(true);
          expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(0);
          success(done, io);
        }, 100);
      });

      io.of(/^\/dynamic-\d+$/);
    });

    it("should NOT clean up namespace when cleanupEmptyChildNamespaces is OFF and client is rejected in middleware", (done) => {
      const io = new Server(0, { cleanupEmptyChildNamespaces: false });
      io.of(/^\/dynamic-\d+$/).use((socket, next) => {
        next(new Error("You shall not pass!"));
      });
      const c1 = createClient(io, "/dynamic-101");

      c1.on("connect", () => {
        done(new Error("Should not connect"));
      });

      c1.on("connect_error", () => {
        setTimeout(() => {
          expect(io._nsps.has("/dynamic-101")).to.be(true);
          expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(0);
          success(done, io, c1);
        }, 100);
      });
    });

    it("should clean up namespace when cleanupEmptyChildNamespaces is ON and client is rejected in middleware", (done) => {
      const io = new Server(0, { cleanupEmptyChildNamespaces: true });
      io.of(/^\/dynamic-\d+$/).use((socket, next) => {
        next(new Error("You shall not pass!"));
      });
      const c1 = createClient(io, "/dynamic-101");

      c1.on("connect", () => {
        done(new Error("Should not connect"));
      });

      c1.on("connect_error", () => {
        setTimeout(() => {
          expect(io._nsps.has("/dynamic-101")).to.be(false);
          success(done, io, c1);
        }, 100);
      });
    });

    it("should NOT clean up namespace when cleanupEmptyChildNamespaces is ON and client is rejected in middleware but there are other clients connected", (done) => {
      const io = new Server(0, { cleanupEmptyChildNamespaces: true });
      let clientIdxToReject = 0;
      io.of(/^\/dynamic-\d+$/).use((socket, next) => {
        if (clientIdxToReject) {
          next(new Error("You shall not pass!"));
        } else {
          next();
        }
        clientIdxToReject++;
      });
      const c1 = createClient(io, "/dynamic-101");

      c1.on("connect", () => {
        const c2 = createClient(io, "/dynamic-101");
        c2.on("connect", () => {
          done(new Error("Client 2 should not connect"));
        });
        c2.on("connect_error", () => {
          setTimeout(() => {
            expect(io._nsps.has("/dynamic-101")).to.be(true);
            expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(1);
            success(done, io, c1, c2);
          }, 100);
        });
      });
      c1.on("connect_error", () => {
        done(new Error("Client 1 should not get an error"));
      });
    });

    it("should attach a child namespace to its parent upon manual creation", () => {
      const io = new Server(0);
      const parentNamespace = io.of(/^\/dynamic-\d+$/);
      const childNamespace = io.of("/dynamic-101");

      // @ts-ignore
      expect(parentNamespace.children.has(childNamespace)).to.be(true);

      io.close();
    });
  });
});
