const VError = require('verror');
const net = require('net');
const http2 = require('http2');

const debug = require('debug')('apn');
const credentials = require('../lib/credentials')({
  logger: debug,
});

const TEST_PORT = 30939;
const LOAD_TEST_BATCH_SIZE = 2000;

const config = require('../lib/config')({
  logger: debug,
  prepareCertificate: () => ({}), // credentials.certificate,
  prepareToken: credentials.token,
  prepareCA: credentials.ca,
});
const Client = require('../lib/client')({
  logger: debug,
  config,
  http2,
});

debug.log = console.log.bind(console);

// function builtNotification() {
//   return {
//     headers: {},
//     body: JSON.stringify({ aps: { badge: 1 } }),
//   };
// }

// function FakeStream(deviceId, statusCode, response) {
//   const fakeStream = new stream.Transform({
//     transform: sinon.spy(function(chunk, encoding, callback) {
//       expect(this.headers).to.be.calledOnce;
//
//       const headers = this.headers.firstCall.args[0];
//       expect(headers[":path"].substring(10)).to.equal(deviceId);
//
//       this.emit("headers", {
//         ":status": statusCode
//       });
//       callback(null, Buffer.from(JSON.stringify(response) || ""));
//     })
//   });
//   fakeStream.headers = sinon.stub();
//
//   return fakeStream;
// }

// XXX these may be flaky in CI due to being sensitive to timing,
// and if a test case crashes, then others may get stuck.
//
// Try to fix this if any issues come up.
describe('Client', () => {
  let server;
  let client;
  const MOCK_BODY = '{"mock-key":"mock-value"}';
  const MOCK_DEVICE_TOKEN = 'abcf0123abcf0123abcf0123abcf0123abcf0123abcf0123abcf0123abcf0123';

  // Create an insecure http2 client for unit testing.
  // (APNS would use https://, not http://)
  // (It's probably possible to allow accepting invalid certificates instead,
  // but that's not the most important point of these tests)
  const createClient = (port, timeout = 500) => {
    const c = new Client({
      port: TEST_PORT,
      address: '127.0.0.1',
    });
    c._mockOverrideUrl = `http://127.0.0.1:${port}`;
    c.config.port = port;
    c.config.address = '127.0.0.1';
    c.config.requestTimeout = timeout;
    return c;
  };
  // Create an insecure server for unit testing.
  const createAndStartMockServer = (port, cb) => {
    server = http2.createServer((req, res) => {
      const buffers = [];
      req.on('data', data => buffers.push(data));
      req.on('end', () => {
        const requestBody = Buffer.concat(buffers).toString('utf-8');
        cb(req, res, requestBody);
      });
    });
    server.listen(port);
    server.on('error', err => {
      expect.fail(`unexpected error ${err}`);
    });
    // Don't block the tests if this server doesn't shut down properly
    server.unref();
    return server;
  };
  const createAndStartMockLowLevelServer = (port, cb) => {
    server = http2.createServer();
    server.on('stream', cb);
    server.listen(port);
    server.on('error', err => {
      expect.fail(`unexpected error ${err}`);
    });
    // Don't block the tests if this server doesn't shut down properly
    server.unref();
    return server;
  };

  afterEach(done => {
    const closeServer = () => {
      if (server) {
        server.close();
        server = null;
      }
      done();
    };
    if (client) {
      client.shutdown(closeServer);
      client = null;
    } else {
      closeServer();
    }
  });

  it('Treats HTTP 200 responses as successful', async () => {
    let didRequest = false;
    let establishedConnections = 0;
    let requestsServed = 0;
    server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
      expect(req.headers).to.deep.equal({
        ':authority': '127.0.0.1',
        ':method': 'POST',
        ':path': `/3/device/${MOCK_DEVICE_TOKEN}`,
        ':scheme': 'https',
        'apns-someheader': 'somevalue',
      });
      expect(requestBody).to.equal(MOCK_BODY);
      // res.setHeader('X-Foo', 'bar');
      // res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.writeHead(200);
      res.end('');
      requestsServed += 1;
      didRequest = true;
    });
    server.on('connection', () => (establishedConnections += 1));
    await new Promise(resolve => server.on('listening', resolve));

    client = createClient(TEST_PORT);

    const runSuccessfulRequest = async () => {
      const mockHeaders = { 'apns-someheader': 'somevalue' };
      const mockNotification = {
        headers: mockHeaders,
        body: MOCK_BODY,
      };
      const mockDevice = MOCK_DEVICE_TOKEN;
      const result = await client.write(mockNotification, mockDevice);
      expect(result).to.deep.equal({ device: MOCK_DEVICE_TOKEN });
      expect(didRequest).to.be.true;
    };
    expect(establishedConnections).to.equal(0); // should not establish a connection until it's needed
    // Validate that when multiple valid requests arrive concurrently,
    // only one HTTP/2 connection gets established
    await Promise.all([
      runSuccessfulRequest(),
      runSuccessfulRequest(),
      runSuccessfulRequest(),
      runSuccessfulRequest(),
      runSuccessfulRequest(),
    ]);
    didRequest = false;
    await runSuccessfulRequest();
    expect(establishedConnections).to.equal(1); // should establish a connection to the server and reuse it
    expect(requestsServed).to.equal(6);
  });

  // Assert that this doesn't crash when a large batch of requests are requested simultaneously
  it('Treats HTTP 200 responses as successful (load test for a batch of requests)', async function () {
    this.timeout(10000);
    let establishedConnections = 0;
    let requestsServed = 0;
    server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
      expect(req.headers).to.deep.equal({
        ':authority': '127.0.0.1',
        ':method': 'POST',
        ':path': `/3/device/${MOCK_DEVICE_TOKEN}`,
        ':scheme': 'https',
        'apns-someheader': 'somevalue',
      });
      expect(requestBody).to.equal(MOCK_BODY);
      // Set a timeout of 100 to simulate latency to a remote server.
      setTimeout(() => {
        res.writeHead(200);
        res.end('');
        requestsServed += 1;
      }, 100);
    });
    server.on('connection', () => (establishedConnections += 1));
    await new Promise(resolve => server.on('listening', resolve));

    client = createClient(TEST_PORT, 1500);

    const runSuccessfulRequest = async () => {
      const mockHeaders = { 'apns-someheader': 'somevalue' };
      const mockNotification = {
        headers: mockHeaders,
        body: MOCK_BODY,
      };
      const mockDevice = MOCK_DEVICE_TOKEN;
      const result = await client.write(mockNotification, mockDevice);
      expect(result).to.deep.equal({ device: MOCK_DEVICE_TOKEN });
    };
    expect(establishedConnections).to.equal(0); // should not establish a connection until it's needed
    // Validate that when multiple valid requests arrive concurrently,
    // only one HTTP/2 connection gets established
    const promises = [];
    for (let i = 0; i < LOAD_TEST_BATCH_SIZE; i++) {
      promises.push(runSuccessfulRequest());
    }

    await Promise.all(promises);
    expect(establishedConnections).to.equal(1); // should establish a connection to the server and reuse it
    expect(requestsServed).to.equal(LOAD_TEST_BATCH_SIZE);
  });

  // https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/handling_notification_responses_from_apns
  it('JSON decodes HTTP 400 responses', async () => {
    let didRequest = false;
    let establishedConnections = 0;
    server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
      expect(requestBody).to.equal(MOCK_BODY);
      // res.setHeader('X-Foo', 'bar');
      // res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.writeHead(400);
      res.end('{"reason": "BadDeviceToken"}');
      didRequest = true;
    });
    server.on('connection', () => (establishedConnections += 1));
    await new Promise(resolve => server.on('listening', resolve));

    client = createClient(TEST_PORT);
    const infoMessages = [];
    const errorMessages = [];
    const mockInfoLogger = message => {
      infoMessages.push(message);
    };
    const mockErrorLogger = message => {
      errorMessages.push(message);
    };
    mockInfoLogger.enabled = true;
    mockErrorLogger.enabled = true;
    client.setLogger(mockInfoLogger, mockErrorLogger);

    const runRequestWithBadDeviceToken = async () => {
      const mockHeaders = { 'apns-someheader': 'somevalue' };
      const mockNotification = {
        headers: mockHeaders,
        body: MOCK_BODY,
      };
      const mockDevice = MOCK_DEVICE_TOKEN;
      const result = await client.write(mockNotification, mockDevice);
      expect(result).to.deep.equal({
        device: MOCK_DEVICE_TOKEN,
        response: {
          reason: 'BadDeviceToken',
        },
        status: 400,
      });
      expect(didRequest).to.be.true;
      didRequest = false;
    };
    await runRequestWithBadDeviceToken();
    await runRequestWithBadDeviceToken();
    expect(establishedConnections).to.equal(1); // should establish a connection to the server and reuse it
    expect(infoMessages).to.deep.equal([
      'Session connected',
      'Request ended with status 400 and responseData: {"reason": "BadDeviceToken"}',
      'Request ended with status 400 and responseData: {"reason": "BadDeviceToken"}',
    ]);
    expect(errorMessages).to.deep.equal([]);
  });

  // node-apn started closing connections in response to a bug report where HTTP 500 responses
  // persisted until a new connection was reopened
  it('Closes connections when HTTP 500 responses are received', async () => {
    let establishedConnections = 0;
    let responseDelay = 50;
    server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
      // Wait 50ms before sending the responses in parallel
      setTimeout(() => {
        expect(requestBody).to.equal(MOCK_BODY);
        res.writeHead(500);
        res.end('{"reason": "InternalServerError"}');
      }, responseDelay);
    });
    server.on('connection', () => (establishedConnections += 1));
    await new Promise(resolve => server.on('listening', resolve));

    client = createClient(TEST_PORT);

    const runRequestWithInternalServerError = async () => {
      const mockHeaders = { 'apns-someheader': 'somevalue' };
      const mockNotification = {
        headers: mockHeaders,
        body: MOCK_BODY,
      };
      const mockDevice = MOCK_DEVICE_TOKEN;
      const result = await client.write(mockNotification, mockDevice);
      expect(result).to.exist;
      expect(result.device).to.equal(MOCK_DEVICE_TOKEN);
      expect(result.error).to.be.an.instanceof(VError);
      expect(result.error.message).to.have.string('stream ended unexpectedly');
    };
    await runRequestWithInternalServerError();
    await runRequestWithInternalServerError();
    await runRequestWithInternalServerError();
    expect(establishedConnections).to.equal(3); // should close and establish new connections on http 500
    // Validate that nothing wrong happens when multiple HTTP 500s are received simultaneously.
    // (no segfaults, all promises get resolved, etc.)
    responseDelay = 50;
    await Promise.all([
      runRequestWithInternalServerError(),
      runRequestWithInternalServerError(),
      runRequestWithInternalServerError(),
      runRequestWithInternalServerError(),
    ]);
    expect(establishedConnections).to.equal(4); // should close and establish new connections on http 500
  });

  it('Handles unexpected invalid JSON responses', async () => {
    let establishedConnections = 0;
    const responseDelay = 0;
    server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
      // Wait 50ms before sending the responses in parallel
      setTimeout(() => {
        expect(requestBody).to.equal(MOCK_BODY);
        res.writeHead(500);
        res.end('PC LOAD LETTER');
      }, responseDelay);
    });
    server.on('connection', () => (establishedConnections += 1));
    await new Promise(resolve => server.on('listening', resolve));

    client = createClient(TEST_PORT);

    const runRequestWithInternalServerError = async () => {
      const mockHeaders = { 'apns-someheader': 'somevalue' };
      const mockNotification = {
        headers: mockHeaders,
        body: MOCK_BODY,
      };
      const mockDevice = MOCK_DEVICE_TOKEN;
      const result = await client.write(mockNotification, mockDevice);
      // Should not happen, but if it does, the promise should resolve with an error
      expect(result.device).to.equal(MOCK_DEVICE_TOKEN);
      expect(result.error.message.startsWith('Unexpected error processing APNs response: Unexpected token')).to.equal(true);
    };
    await runRequestWithInternalServerError();
    await runRequestWithInternalServerError();
    expect(establishedConnections).to.equal(1); // Currently reuses the connection.
  });

  it('Handles APNs timeouts', async () => {
    let didGetRequest = false;
    let didGetResponse = false;
    server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
      didGetRequest = true;
      setTimeout(() => {
        res.writeHead(200);
        res.end('');
        didGetResponse = true;
      }, 1900);
    });
    client = createClient(TEST_PORT);

    const onListeningPromise = new Promise(resolve => server.on('listening', resolve));
    await onListeningPromise;

    const mockHeaders = { 'apns-someheader': 'somevalue' };
    const mockNotification = {
      headers: mockHeaders,
      body: MOCK_BODY,
    };
    const mockDevice = MOCK_DEVICE_TOKEN;
    const performRequestExpectingTimeout = async () => {
      const result = await client.write(mockNotification, mockDevice);
      expect(result).to.deep.equal({
        device: MOCK_DEVICE_TOKEN,
        error: new VError('apn write timeout'),
      });
      expect(didGetRequest).to.be.true;
      expect(didGetResponse).to.be.false;
    };
    await performRequestExpectingTimeout();
    didGetResponse = false;
    didGetRequest = false;
    // Should be able to have multiple in flight requests all get notified that the server is shutting down
    await Promise.all([
      performRequestExpectingTimeout(),
      performRequestExpectingTimeout(),
      performRequestExpectingTimeout(),
      performRequestExpectingTimeout(),
    ]);
  });

  it('Handles goaway frames', async () => {
    let didGetRequest = false;
    let establishedConnections = 0;
    server = createAndStartMockLowLevelServer(TEST_PORT, stream => {
      const { session } = stream;
      const errorCode = 1;
      didGetRequest = true;
      session.goaway(errorCode);
    });
    server.on('connection', () => (establishedConnections += 1));
    client = createClient(TEST_PORT);

    const onListeningPromise = new Promise(resolve => server.on('listening', resolve));
    await onListeningPromise;

    const mockHeaders = { 'apns-someheader': 'somevalue' };
    const mockNotification = {
      headers: mockHeaders,
      body: MOCK_BODY,
    };
    const mockDevice = MOCK_DEVICE_TOKEN;
    const performRequestExpectingGoAway = async () => {
      const result = await client.write(mockNotification, mockDevice);
      expect(result.device).to.equal(MOCK_DEVICE_TOKEN);
      expect(result.error).to.be.an.instanceof(VError);
      expect(didGetRequest).to.be.true;
      didGetRequest = false;
    };
    await performRequestExpectingGoAway();
    await performRequestExpectingGoAway();
    expect(establishedConnections).to.equal(2);
  });

  it('Handles unexpected protocol errors (no response sent)', async () => {
    let didGetRequest = false;
    let establishedConnections = 0;
    let responseTimeout = 0;
    server = createAndStartMockLowLevelServer(TEST_PORT, stream => {
      setTimeout(() => {
        const { session } = stream;
        didGetRequest = true;
        if (session) {
          session.destroy();
        }
      }, responseTimeout);
    });
    server.on('connection', () => (establishedConnections += 1));
    client = createClient(TEST_PORT);

    const onListeningPromise = new Promise(resolve => server.on('listening', resolve));
    await onListeningPromise;

    const mockHeaders = { 'apns-someheader': 'somevalue' };
    const mockNotification = {
      headers: mockHeaders,
      body: MOCK_BODY,
    };
    const mockDevice = MOCK_DEVICE_TOKEN;
    const performRequestExpectingDisconnect = async () => {
      const result = await client.write(mockNotification, mockDevice);
      expect(result).to.deep.equal({
        device: MOCK_DEVICE_TOKEN,
        error: new VError('stream ended unexpectedly with status null and empty body'),
      });
      expect(didGetRequest).to.be.true;
    };
    await performRequestExpectingDisconnect();
    didGetRequest = false;
    await performRequestExpectingDisconnect();
    didGetRequest = false;
    expect(establishedConnections).to.equal(2);
    responseTimeout = 10;
    await Promise.all([
      performRequestExpectingDisconnect(),
      performRequestExpectingDisconnect(),
      performRequestExpectingDisconnect(),
      performRequestExpectingDisconnect(),
    ]);
    expect(establishedConnections).to.equal(3);
  });

  it('Establishes a connection through a proxy server', async () => {
    let didRequest = false;
    let establishedConnections = 0;
    let requestsServed = 0;
    server = createAndStartMockServer(TEST_PORT, (req, res, requestBody) => {
      expect(req.headers).to.deep.equal({
        ':authority': '127.0.0.1',
        ':method': 'POST',
        ':path': `/3/device/${MOCK_DEVICE_TOKEN}`,
        ':scheme': 'https',
        'apns-someheader': 'somevalue',
      });
      expect(requestBody).to.equal(MOCK_BODY);
      // res.setHeader('X-Foo', 'bar');
      // res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.writeHead(200);
      res.end('');
      requestsServed += 1;
      didRequest = true;
    });
    server.on('connection', () => (establishedConnections += 1));
    await new Promise(resolve => server.once('listening', resolve));

    // Proxy forwards all connections to TEST_PORT
    const proxy = net.createServer(clientSocket => {
      clientSocket.once('data', () => {
        const serverSocket = net.createConnection(TEST_PORT, () => {
          clientSocket.write('HTTP/1.1 200 OK\r\n\r\n');
          clientSocket.pipe(serverSocket);
          setTimeout(() => {
            serverSocket.pipe(clientSocket);
          }, 1);
        });
      });
      clientSocket.on('error', () => {});
    });
    await new Promise(resolve => proxy.listen(3128, resolve));

    // Client configured with a port that the server is not listening on
    client = createClient(TEST_PORT + 1);
    // So without adding a proxy config request will fail with a network error
    client.config.proxy = { host: '127.0.0.1', port: 3128 };
    const runSuccessfulRequest = async () => {
      const mockHeaders = { 'apns-someheader': 'somevalue' };
      const mockNotification = {
        headers: mockHeaders,
        body: MOCK_BODY,
      };
      const mockDevice = MOCK_DEVICE_TOKEN;
      const result = await client.write(mockNotification, mockDevice);
      expect(result).to.deep.equal({ device: MOCK_DEVICE_TOKEN });
      expect(didRequest).to.be.true;
    };
    expect(establishedConnections).to.equal(0); // should not establish a connection until it's needed
    // Validate that when multiple valid requests arrive concurrently,
    // only one HTTP/2 connection gets established
    await Promise.all([
      runSuccessfulRequest(),
      runSuccessfulRequest(),
      runSuccessfulRequest(),
      runSuccessfulRequest(),
      runSuccessfulRequest(),
    ]);
    didRequest = false;
    await runSuccessfulRequest();
    expect(establishedConnections).to.equal(1); // should establish a connection to the server and reuse it
    expect(requestsServed).to.equal(6);

    proxy.close();
  });

  // let fakes, Client;

  // beforeEach(() => {
  //   fakes = {
  //     config: sinon.stub(),
  //     EndpointManager: sinon.stub(),
  //     endpointManager: new EventEmitter(),
  //   };

  //   fakes.EndpointManager.returns(fakes.endpointManager);
  //   fakes.endpointManager.shutdown = sinon.stub();

  //   Client = require("../lib/client")(fakes);
  // });

  // describe("constructor", () => {
  //   it("prepares the configuration with passed options", () => {
  //     let options = { production: true };
  //     let client = new Client(options);

  //     expect(fakes.config).to.be.calledWith(options);
  //   });

  //   describe("EndpointManager instance", function() {
  //     it("is created", () => {
  //       let client = new Client();

  //       expect(fakes.EndpointManager).to.be.calledOnce;
  //       expect(fakes.EndpointManager).to.be.calledWithNew;
  //     });

  //     it("is passed the prepared configuration", () => {
  //       const returnSentinel = { "configKey": "configValue"};
  //       fakes.config.returns(returnSentinel);

  //       let client = new Client({});
  //       expect(fakes.EndpointManager).to.be.calledWith(returnSentinel);
  //     });
  //   });
  // });

  describe('write', () => {
    // beforeEach(() => {
    //   fakes.config.returnsArg(0);
    //   fakes.endpointManager.getStream = sinon.stub();
    //   fakes.EndpointManager.returns(fakes.endpointManager);
    // });
    // context("a stream is available", () => {
    //   let client;
    //   context("transmission succeeds", () => {
    //     beforeEach( () => {
    //       client = new Client( { address: "testapi" } );
    //       fakes.stream = new FakeStream("abcd1234", "200");
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //     });
    //     it("attempts to acquire one stream", () => {
    //       return client.write(builtNotification(), "abcd1234")
    //         .then(() => {
    //           expect(fakes.endpointManager.getStream).to.be.calledOnce;
    //         });
    //     });
    //     describe("headers", () => {
    //       it("sends the required HTTP/2 headers", () => {
    //         return client.write(builtNotification(), "abcd1234")
    //           .then(() => {
    //             expect(fakes.stream.headers).to.be.calledWithMatch( {
    //               ":scheme": "https",
    //               ":method": "POST",
    //               ":authority": "testapi",
    //               ":path": "/3/device/abcd1234",
    //             });
    //           });
    //       });
    //       it("does not include apns headers when not required", () => {
    //         return client.write(builtNotification(), "abcd1234")
    //           .then(() => {
    //             ["apns-id", "apns-priority", "apns-expiration", "apns-topic"].forEach( header => {
    //               expect(fakes.stream.headers).to.not.be.calledWithMatch(sinon.match.has(header));
    //             });
    //           });
    //       });
    //       it("sends the notification-specific apns headers when specified", () => {
    //         let notification = builtNotification();
    //         notification.headers = {
    //           "apns-id": "123e4567-e89b-12d3-a456-42665544000",
    //           "apns-priority": 5,
    //           "apns-expiration": 123,
    //           "apns-topic": "io.apn.node",
    //         };
    //         return client.write(notification, "abcd1234")
    //           .then(() => {
    //             expect(fakes.stream.headers).to.be.calledWithMatch( {
    //               "apns-id": "123e4567-e89b-12d3-a456-42665544000",
    //               "apns-priority": 5,
    //               "apns-expiration": 123,
    //               "apns-topic": "io.apn.node",
    //             });
    //           });
    //       });
    //       context("when token authentication is enabled", () => {
    //         beforeEach(() => {
    //           fakes.token = {
    //             generation: 0,
    //             current: "fake-token",
    //             regenerate: sinon.stub(),
    //             isExpired: sinon.stub()
    //           };
    //           client = new Client( { address: "testapi", token: fakes.token } );
    //           fakes.stream = new FakeStream("abcd1234", "200");
    //           fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //         });
    //         it("sends the bearer token", () => {
    //           let notification = builtNotification();
    //           return client.write(notification, "abcd1234").then(() => {
    //             expect(fakes.stream.headers).to.be.calledWithMatch({
    //               authorization: "bearer fake-token",
    //             });
    //           });
    //         });
    //       });
    //       context("when token authentication is disabled", () => {
    //         beforeEach(() => {
    //           client = new Client( { address: "testapi" } );
    //           fakes.stream = new FakeStream("abcd1234", "200");
    //           fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //         });
    //         it("does not set an authorization header", () => {
    //           let notification = builtNotification();
    //           return client.write(notification, "abcd1234").then(() => {
    //             expect(fakes.stream.headers.firstCall.args[0]).to.not.have.property("authorization");
    //           })
    //         });
    //       })
    //     });
    //     it("writes the notification data to the pipe", () => {
    //       const notification = builtNotification();
    //       return client.write(notification, "abcd1234")
    //         .then(() => {
    //           expect(fakes.stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer.from(notification.body)));
    //         });
    //     });
    //     it("ends the stream", () => {
    //       sinon.spy(fakes.stream, "end");
    //       return client.write(builtNotification(), "abcd1234")
    //         .then(() => {
    //           expect(fakes.stream.end).to.be.calledOnce;
    //         });
    //     });
    //     it("resolves with the device token", () => {
    //       return expect(client.write(builtNotification(), "abcd1234"))
    //         .to.become({ device: "abcd1234" });
    //     });
    //   });
    //   context("error occurs", () => {
    //     let promise;
    //     context("general case", () => {
    //       beforeEach(() => {
    //         const client = new Client( { address: "testapi" } );
    //         fakes.stream = new FakeStream("abcd1234", "400", { "reason" : "BadDeviceToken" });
    //         fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //         promise = client.write(builtNotification(), "abcd1234");
    //       });
    //       it("resolves with the device token, status code and response", () => {
    //         return expect(promise).to.eventually.deep.equal({ status: "400", device: "abcd1234", response: { reason: "BadDeviceToken" }});
    //       });
    //     })
    //     context("ExpiredProviderToken", () => {
    //       beforeEach(() => {
    //         let tokenGenerator = sinon.stub().returns("fake-token");
    //         const client = new Client( { address: "testapi", token: tokenGenerator });
    //       })
    //     });
    //   });
    //   context("stream ends without completing request", () => {
    //     let promise;
    //     beforeEach(() => {
    //       const client = new Client( { address: "testapi" } );
    //       fakes.stream = new stream.Transform({
    //         transform: function(chunk, encoding, callback) {}
    //       });
    //       fakes.stream.headers = sinon.stub();
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //       promise = client.write(builtNotification(), "abcd1234");
    //       fakes.stream.push(null);
    //     });
    //     it("resolves with an object containing the device token", () => {
    //       return expect(promise).to.eventually.have.property("device", "abcd1234");
    //     });
    //     it("resolves with an object containing an error", () => {
    //       return promise.then( (response) => {
    //         expect(response).to.have.property("error");
    //         expect(response.error).to.be.an.instanceOf(Error);
    //         expect(response.error).to.match(/stream ended unexpectedly/);
    //       });
    //     });
    //   });
    //   context("stream is unprocessed", () => {
    //     let promise;
    //     beforeEach(() => {
    //       const client = new Client( { address: "testapi" } );
    //       fakes.stream = new stream.Transform({
    //         transform: function(chunk, encoding, callback) {}
    //       });
    //       fakes.stream.headers = sinon.stub();
    //       fakes.secondStream = FakeStream("abcd1234", "200");
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //       fakes.endpointManager.getStream.onCall(1).returns(fakes.secondStream);
    //       promise = client.write(builtNotification(), "abcd1234");
    //       setImmediate(() => {
    //         fakes.stream.emit("unprocessed");
    //       });
    //     });
    //     it("attempts to resend on a new stream", function (done) {
    //       setImmediate(() => {
    //         expect(fakes.endpointManager.getStream).to.be.calledTwice;
    //         done();
    //       });
    //     });
    //     it("fulfills the promise", () => {
    //       return expect(promise).to.eventually.deep.equal({ device: "abcd1234"  });
    //     });
    //   });
    //   context("stream error occurs", () => {
    //     let promise;
    //     beforeEach(() => {
    //       const client = new Client( { address: "testapi" } );
    //       fakes.stream = new stream.Transform({
    //         transform: function(chunk, encoding, callback) {}
    //       });
    //       fakes.stream.headers = sinon.stub();
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //       promise = client.write(builtNotification(), "abcd1234");
    //     });
    //     context("passing an Error", () => {
    //       beforeEach(() => {
    //         fakes.stream.emit("error", new Error("stream error"));
    //       });
    //       it("resolves with an object containing the device token", () => {
    //         return expect(promise).to.eventually.have.property("device", "abcd1234");
    //       });
    //       it("resolves with an object containing a wrapped error", () => {
    //         return promise.then( (response) => {
    //           expect(response.error).to.be.an.instanceOf(Error);
    //           expect(response.error).to.match(/apn write failed/);
    //           expect(response.error.cause()).to.be.an.instanceOf(Error).and.match(/stream error/);
    //         });
    //       });
    //     });
    //     context("passing a string", () => {
    //       it("resolves with the device token and an error", () => {
    //         fakes.stream.emit("error", "stream error");
    //         return promise.then( (response) => {
    //             expect(response).to.have.property("device", "abcd1234");
    //             expect(response.error).to.to.be.an.instanceOf(Error);
    //             expect(response.error).to.match(/apn write failed/);
    //             expect(response.error).to.match(/stream error/);
    //         });
    //       });
    //     });
    //   });
    // });
    // context("no new stream is returned but the endpoint later wakes up", () => {
    //   let notification, promise;
    //   beforeEach( () => {
    //     const client = new Client( { address: "testapi" } );
    //     fakes.stream = new FakeStream("abcd1234", "200");
    //     fakes.endpointManager.getStream.onCall(0).returns(null);
    //     fakes.endpointManager.getStream.onCall(1).returns(fakes.stream);
    //     notification = builtNotification();
    //     promise = client.write(notification, "abcd1234");
    //     expect(fakes.stream.headers).to.not.be.called;
    //     fakes.endpointManager.emit("wakeup");
    //     return promise;
    //   });
    //   it("sends the required headers to the newly available stream", () => {
    //     expect(fakes.stream.headers).to.be.calledWithMatch( {
    //       ":scheme": "https",
    //       ":method": "POST",
    //       ":authority": "testapi",
    //       ":path": "/3/device/abcd1234",
    //     });
    //   });
    //   it("writes the notification data to the pipe", () => {
    //     expect(fakes.stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer.from(notification.body)));
    //   });
    // });
    // context("when 5 successive notifications are sent", () => {
    //   beforeEach(() => {
    //       fakes.streams = [
    //         new FakeStream("abcd1234", "200"),
    //         new FakeStream("adfe5969", "400", { reason: "MissingTopic" }),
    //         new FakeStream("abcd1335", "410", { reason: "BadDeviceToken", timestamp: 123456789 }),
    //         new FakeStream("bcfe4433", "200"),
    //         new FakeStream("aabbc788", "413", { reason: "PayloadTooLarge" }),
    //       ];
    //   });
    //   context("streams are always returned", () => {
    //     let promises;
    //     beforeEach( () => {
    //       const client = new Client( { address: "testapi" } );
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //       fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
    //       fakes.endpointManager.getStream.onCall(2).returns(fakes.streams[2]);
    //       fakes.endpointManager.getStream.onCall(3).returns(fakes.streams[3]);
    //       fakes.endpointManager.getStream.onCall(4).returns(fakes.streams[4]);
    //       promises = Promise.all([
    //         client.write(builtNotification(), "abcd1234"),
    //         client.write(builtNotification(), "adfe5969"),
    //         client.write(builtNotification(), "abcd1335"),
    //         client.write(builtNotification(), "bcfe4433"),
    //         client.write(builtNotification(), "aabbc788"),
    //       ]);
    //       return promises;
    //     });
    //     it("sends the required headers for each stream", () => {
    //       expect(fakes.streams[0].headers).to.be.calledWithMatch( { ":path": "/3/device/abcd1234" } );
    //       expect(fakes.streams[1].headers).to.be.calledWithMatch( { ":path": "/3/device/adfe5969" } );
    //       expect(fakes.streams[2].headers).to.be.calledWithMatch( { ":path": "/3/device/abcd1335" } );
    //       expect(fakes.streams[3].headers).to.be.calledWithMatch( { ":path": "/3/device/bcfe4433" } );
    //       expect(fakes.streams[4].headers).to.be.calledWithMatch( { ":path": "/3/device/aabbc788" } );
    //     });
    //     it("writes the notification data for each stream", () => {
    //       fakes.streams.forEach( stream => {
    //         expect(stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer.from(builtNotification().body)));
    //       });
    //     });
    //     it("resolves with the notification outcomes", () => {
    //       return expect(promises).to.eventually.deep.equal([
    //           { device: "abcd1234"},
    //           { device: "adfe5969", status: "400", response: { reason: "MissingTopic" } },
    //           { device: "abcd1335", status: "410", response: { reason: "BadDeviceToken", timestamp: 123456789 } },
    //           { device: "bcfe4433"},
    //           { device: "aabbc788", status: "413", response: { reason: "PayloadTooLarge" } },
    //       ]);
    //     });
    //   });
    //   context("some streams return, others wake up later", () => {
    //     let promises;
    //     beforeEach( function() {
    //       const client = new Client( { address: "testapi" } );
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //       fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
    //       promises = Promise.all([
    //         client.write(builtNotification(), "abcd1234"),
    //         client.write(builtNotification(), "adfe5969"),
    //         client.write(builtNotification(), "abcd1335"),
    //         client.write(builtNotification(), "bcfe4433"),
    //         client.write(builtNotification(), "aabbc788"),
    //       ]);
    //       setTimeout(() => {
    //         fakes.endpointManager.getStream.reset();
    //         fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[2]);
    //         fakes.endpointManager.getStream.onCall(1).returns(null);
    //         fakes.endpointManager.emit("wakeup");
    //       }, 1);
    //       setTimeout(() => {
    //         fakes.endpointManager.getStream.reset();
    //         fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[3]);
    //         fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[4]);
    //         fakes.endpointManager.emit("wakeup");
    //       }, 2);
    //       return promises;
    //     });
    //     it("sends the correct device ID for each stream", () => {
    //       expect(fakes.streams[0].headers).to.be.calledWithMatch({":path": "/3/device/abcd1234"});
    //       expect(fakes.streams[1].headers).to.be.calledWithMatch({":path": "/3/device/adfe5969"});
    //       expect(fakes.streams[2].headers).to.be.calledWithMatch({":path": "/3/device/abcd1335"});
    //       expect(fakes.streams[3].headers).to.be.calledWithMatch({":path": "/3/device/bcfe4433"});
    //       expect(fakes.streams[4].headers).to.be.calledWithMatch({":path": "/3/device/aabbc788"});
    //     });
    //     it("writes the notification data for each stream", () => {
    //       fakes.streams.forEach( stream => {
    //         expect(stream._transform).to.be.calledWithMatch(actual => actual.equals(Buffer.from(builtNotification().body)));
    //       });
    //     });
    //     it("resolves with the notification reponses", () => {
    //       return expect(promises).to.eventually.deep.equal([
    //           { device: "abcd1234"},
    //           { device: "adfe5969", status: "400", response: { reason: "MissingTopic" } },
    //           { device: "abcd1335", status: "410", response: { reason: "BadDeviceToken", timestamp: 123456789 } },
    //           { device: "bcfe4433"},
    //           { device: "aabbc788", status: "413", response: { reason: "PayloadTooLarge" } },
    //       ]);
    //     });
    //   });
    //   context("connection fails", () => {
    //     let promises, client;
    //     beforeEach( function() {
    //       client = new Client( { address: "testapi" } );
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //       promises = Promise.all([
    //         client.write(builtNotification(), "abcd1234"),
    //         client.write(builtNotification(), "adfe5969"),
    //         client.write(builtNotification(), "abcd1335"),
    //       ]);
    //       setTimeout(() => {
    //         fakes.endpointManager.getStream.reset();
    //         fakes.endpointManager.emit("error", new Error("endpoint failed"));
    //       }, 1);
    //       return promises;
    //     });
    //     it("resolves with 1 success", () => {
    //       return promises.then( response => {
    //         expect(response[0]).to.deep.equal({ device: "abcd1234" });
    //       });
    //     });
    //     it("resolves with 2 errors", () => {
    //       return promises.then( response => {
    //         expect(response[1]).to.deep.equal({ device: "adfe5969", error: new Error("endpoint failed") });
    //         expect(response[2]).to.deep.equal({ device: "abcd1335", error: new Error("endpoint failed") });
    //       })
    //     });
    //     it("clears the queue", () => {
    //       return promises.then( () => {
    //         expect(client.queue.length).to.equal(0);
    //       });
    //     });
    //   });
    // });
    // describe("token generator behaviour", () => {
    //   beforeEach(() => {
    //     fakes.token = {
    //       generation: 0,
    //       current: "fake-token",
    //       regenerate: sinon.stub(),
    //       isExpired: sinon.stub()
    //     }
    //     fakes.streams = [
    //       new FakeStream("abcd1234", "200"),
    //       new FakeStream("adfe5969", "400", { reason: "MissingTopic" }),
    //       new FakeStream("abcd1335", "410", { reason: "BadDeviceToken", timestamp: 123456789 }),
    //     ];
    //   });
    //   it("reuses the token", () => {
    //     const client = new Client( { address: "testapi", token: fakes.token } );
    //     fakes.token.regenerate = () => {
    //       fakes.token.generation = 1;
    //       fakes.token.current = "second-token";
    //     }
    //     fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //     fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
    //     fakes.endpointManager.getStream.onCall(2).returns(fakes.streams[2]);
    //     return Promise.all([
    //       client.write(builtNotification(), "abcd1234"),
    //       client.write(builtNotification(), "adfe5969"),
    //       client.write(builtNotification(), "abcd1335"),
    //     ]).then(() => {
    //       expect(fakes.streams[0].headers).to.be.calledWithMatch({ authorization: "bearer fake-token" });
    //       expect(fakes.streams[1].headers).to.be.calledWithMatch({ authorization: "bearer fake-token" });
    //       expect(fakes.streams[2].headers).to.be.calledWithMatch({ authorization: "bearer fake-token" });
    //     });
    //   });
    //   context("token expires", () => {
    //     beforeEach(() => {
    //       fakes.token.regenerate = function (generation) {
    //         if (generation === fakes.token.generation) {
    //           fakes.token.generation += 1;
    //           fakes.token.current = "token-" + fakes.token.generation;
    //         }
    //       }
    //     });
    //     it("resends the notification with a new token", () => {
    //       fakes.streams = [
    //         new FakeStream("adfe5969", "403", { reason: "ExpiredProviderToken" }),
    //         new FakeStream("adfe5969", "200"),
    //       ];
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //       const client = new Client( { address: "testapi", token: fakes.token } );
    //       const promise = client.write(builtNotification(), "adfe5969");
    //       setTimeout(() => {
    //         fakes.endpointManager.getStream.reset();
    //         fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[1]);
    //         fakes.endpointManager.emit("wakeup");
    //       }, 1);
    //       return promise.then(() => {
    //         expect(fakes.streams[0].headers).to.be.calledWithMatch({ authorization: "bearer fake-token" });
    //         expect(fakes.streams[1].headers).to.be.calledWithMatch({ authorization: "bearer token-1" });
    //       });
    //     });
    //     it("only regenerates the token once per-expiry", () => {
    //       fakes.streams = [
    //         new FakeStream("abcd1234", "200"),
    //         new FakeStream("adfe5969", "403", { reason: "ExpiredProviderToken" }),
    //         new FakeStream("abcd1335", "403", { reason: "ExpiredProviderToken" }),
    //         new FakeStream("adfe5969", "400", { reason: "MissingTopic" }),
    //         new FakeStream("abcd1335", "410", { reason: "BadDeviceToken", timestamp: 123456789 }),
    //       ];
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //       fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
    //       fakes.endpointManager.getStream.onCall(2).returns(fakes.streams[2]);
    //       const client = new Client( { address: "testapi", token: fakes.token } );
    //       const promises = Promise.all([
    //         client.write(builtNotification(), "abcd1234"),
    //         client.write(builtNotification(), "adfe5969"),
    //         client.write(builtNotification(), "abcd1335"),
    //       ]);
    //       setTimeout(() => {
    //         fakes.endpointManager.getStream.reset();
    //         fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[3]);
    //         fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[4]);
    //         fakes.endpointManager.emit("wakeup");
    //       }, 1);
    //       return promises.then(() => {
    //         expect(fakes.streams[0].headers).to.be.calledWithMatch({ authorization: "bearer fake-token" });
    //         expect(fakes.streams[1].headers).to.be.calledWithMatch({ authorization: "bearer fake-token" });
    //         expect(fakes.streams[2].headers).to.be.calledWithMatch({ authorization: "bearer fake-token" });
    //         expect(fakes.streams[3].headers).to.be.calledWithMatch({ authorization: "bearer token-1" });
    //         expect(fakes.streams[4].headers).to.be.calledWithMatch({ authorization: "bearer token-1" });
    //       });
    //     });
    //     it("abandons sending after 3 ExpiredProviderToken failures", () => {
    //       fakes.streams = [
    //         new FakeStream("adfe5969", "403", { reason: "ExpiredProviderToken" }),
    //         new FakeStream("adfe5969", "403", { reason: "ExpiredProviderToken" }),
    //         new FakeStream("adfe5969", "403", { reason: "ExpiredProviderToken" }),
    //       ];
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //       fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
    //       fakes.endpointManager.getStream.onCall(2).returns(fakes.streams[2]);
    //       const client = new Client( { address: "testapi", token: fakes.token } );
    //       return expect(client.write(builtNotification(), "adfe5969")).to.eventually.have.property("status", "403");
    //     });
    //     it("regenerate token", () => {
    //       fakes.stream = new FakeStream("abcd1234", "200");
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //       fakes.token.isExpired = function (current, validSeconds) {
    //         return true;
    //       }
    //       let client = new Client({
    //         address: "testapi",
    //         token: fakes.token
    //       });
    //       return client.write(builtNotification(), "abcd1234")
    //         .then(() => {
    //           expect(fakes.token.generation).to.equal(1);
    //         });
    //     });
    //     it("internal server error", () => {
    //       fakes.stream = new FakeStream("abcd1234", "500", { reason: "InternalServerError" });
    //       fakes.stream.connection = sinon.stub();
    //       fakes.stream.connection.close = sinon.stub();
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.stream);
    //       let client = new Client({
    //         address: "testapi",
    //         token: fakes.token
    //       });
    //       return expect(client.write(builtNotification(), "abcd1234")).to.eventually.have.deep.property("error.jse_shortmsg","Error 500, stream ended unexpectedly");
    //     });
    //   });
    // });
  });

  describe('shutdown', () => {
    // beforeEach(() => {
    //   fakes.config.returnsArg(0);
    //   fakes.endpointManager.getStream = sinon.stub();
    //   fakes.EndpointManager.returns(fakes.endpointManager);
    // });
    // context("with no pending notifications", () => {
    //   it("invokes shutdown on endpoint manager", () => {
    //     let client = new Client();
    //     client.shutdown();
    //     expect(fakes.endpointManager.shutdown).to.be.calledOnce;
    //   });
    // });
    // context("with pending notifications", () => {
    //   it("invokes shutdown on endpoint manager after queue drains", () => {
    //     let client = new Client({ address: "none" });
    //     fakes.streams = [
    //       new FakeStream("abcd1234", "200"),
    //       new FakeStream("adfe5969", "400", { reason: "MissingTopic" }),
    //       new FakeStream("abcd1335", "410", { reason: "BadDeviceToken", timestamp: 123456789 }),
    //       new FakeStream("bcfe4433", "200"),
    //       new FakeStream("aabbc788", "413", { reason: "PayloadTooLarge" }),
    //     ];
    //     fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[0]);
    //     fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[1]);
    //     let promises = Promise.all([
    //       client.write(builtNotification(), "abcd1234"),
    //       client.write(builtNotification(), "adfe5969"),
    //       client.write(builtNotification(), "abcd1335"),
    //       client.write(builtNotification(), "bcfe4433"),
    //       client.write(builtNotification(), "aabbc788"),
    //     ]);
    //     client.shutdown();
    //     expect(fakes.endpointManager.shutdown).to.not.be.called;
    //     setTimeout(() => {
    //       fakes.endpointManager.getStream.reset();
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[2]);
    //       fakes.endpointManager.getStream.onCall(1).returns(null);
    //       fakes.endpointManager.emit("wakeup");
    //     }, 1);
    //     setTimeout(() => {
    //       fakes.endpointManager.getStream.reset();
    //       fakes.endpointManager.getStream.onCall(0).returns(fakes.streams[3]);
    //       fakes.endpointManager.getStream.onCall(1).returns(fakes.streams[4]);
    //       fakes.endpointManager.emit("wakeup");
    //     }, 2);
    //     return promises.then( () => {
    //       expect(fakes.endpointManager.shutdown).to.have.been.called;
    //     });
    //   });
    // });
  });
});
