// Copyright 2021-2023 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { useNodeServer } from "./use-node-server-helper.spec.js";
import * as http2 from "http2";
import * as http from "http";
import { universalRequestFromNodeRequest } from "./node-universal-handler.js";
import { DubboError } from "@apachedubbo/dubbo";
import { getNodeErrorProps } from "./node-error.js";
import {
  assertByteStreamRequest,
  readAllBytes,
  type UniversalServerRequest,
} from "@apachedubbo/dubbo/protocol";

// Polyfill the Headers API for Node versions < 18
import "./node-headers-polyfill.js";

describe("universalRequestFromNodeRequest()", function () {
  describe("with HTTP/2 stream closed with an RST code", function () {
    let serverRequest: UniversalServerRequest | undefined;
    const server = useNodeServer(() => {
      serverRequest = undefined;
      return http2.createServer(function (request) {
        serverRequest = universalRequestFromNodeRequest(request, undefined);
      });
    });
    async function request(rstCode: number) {
      await new Promise<void>((resolve) => {
        http2.connect(server.getUrl(), (session: http2.ClientHttp2Session) => {
          const stream = session.request(
            {
              ":method": "POST",
              ":path": "/",
            },
            {}
          );
          setTimeout(() => {
            stream.on("error", () => {
              // Closing with _some_ codes raises an ERR_HTTP2_STREAM_ERROR
              // error here.
            });
            stream.close(rstCode, () => {
              // We are seeing a race condition in Node v16.20.0, where closing
              // the session right after closing a stream with an RST code
              // _sometimes_ sends an INTERNAL_ERROR code.
              // Simply delaying the session close until the next tick like
              // we do here seems to work around the issue.
              setTimeout(() => session.close(resolve), 0);
            });
          }, 0);
        });
      });
    }
    it("should abort request signal with DubboError and Code.Canceled for NO_ERROR", async function () {
      await request(http2.constants.NGHTTP2_NO_ERROR);
      expect(serverRequest?.signal).toBeInstanceOf(AbortSignal);
      expect(serverRequest?.signal.aborted).toBeTrue();
      expect(serverRequest?.signal.reason).toBeInstanceOf(Error);
      if (serverRequest?.signal.reason instanceof Error) {
        expect(serverRequest.signal.reason.name).toBe("AbortError");
        expect(serverRequest.signal.reason.message).toBe(
          "This operation was aborted"
        );
      }
    });
    it("should silently end request body stream for NO_ERROR", async function () {
      await request(http2.constants.NGHTTP2_NO_ERROR);
      expect(serverRequest).toBeDefined();
      if (serverRequest !== undefined) {
        assertByteStreamRequest(serverRequest);
        const it = serverRequest.body[Symbol.asyncIterator]();
        const r = await it.next();
        expect(r.done).toBeTrue();
      }
    });
    it("should abort request signal with DubboError and Code.Canceled for CANCEL", async function () {
      await request(http2.constants.NGHTTP2_CANCEL);
      expect(serverRequest?.signal).toBeInstanceOf(AbortSignal);
      expect(serverRequest?.signal.aborted).toBeTrue();
      expect(serverRequest?.signal.reason).toBeInstanceOf(DubboError);
      const ce = DubboError.from(serverRequest?.signal.reason);
      expect(ce.message).toBe(
        "[canceled] http/2 stream closed with error code CANCEL (0x8)"
      );
    });
    it("should silently end request body stream for CANCEL", async function () {
      await request(http2.constants.NGHTTP2_CANCEL);
      expect(serverRequest).toBeDefined();
      if (serverRequest !== undefined) {
        assertByteStreamRequest(serverRequest);
        const it = serverRequest.body[Symbol.asyncIterator]();
        const r = await it.next();
        expect(r.done).toBeTrue();
      }
    });
    it("should abort request signal with DubboError and Code.ResourceExhausted for ENHANCE_YOUR_CALM", async function () {
      await request(http2.constants.NGHTTP2_ENHANCE_YOUR_CALM);
      expect(serverRequest?.signal).toBeInstanceOf(AbortSignal);
      expect(serverRequest?.signal.aborted).toBeTrue();
      expect(serverRequest?.signal.reason).toBeInstanceOf(DubboError);
      const ce = DubboError.from(serverRequest?.signal.reason);
      expect(ce.message).toBe(
        "[resource_exhausted] http/2 stream closed with error code ENHANCE_YOUR_CALM (0xb)"
      );
    });
    it("should silently end request body stream for ENHANCE_YOUR_CALM", async function () {
      await request(http2.constants.NGHTTP2_ENHANCE_YOUR_CALM);
      expect(serverRequest).toBeDefined();
      if (serverRequest !== undefined) {
        assertByteStreamRequest(serverRequest);
        const it = serverRequest.body[Symbol.asyncIterator]();
        const r = await it.next();
        expect(r.done).toBeTrue();
      }
    });
    it("should abort request signal with DubboError and Code.Internal for FRAME_SIZE_ERROR", async function () {
      await request(http2.constants.NGHTTP2_FRAME_SIZE_ERROR);
      expect(serverRequest?.signal).toBeInstanceOf(AbortSignal);
      expect(serverRequest?.signal.aborted).toBeTrue();
      expect(serverRequest?.signal.reason).toBeInstanceOf(DubboError);
      const ce = DubboError.from(serverRequest?.signal.reason);
      expect(ce.message).toBe(
        "[internal] http/2 stream closed with error code FRAME_SIZE_ERROR (0x6)"
      );
    });
    it("should silently end request body stream for FRAME_SIZE_ERROR", async function () {
      await request(http2.constants.NGHTTP2_FRAME_SIZE_ERROR);
      expect(serverRequest).toBeDefined();
      if (serverRequest !== undefined) {
        assertByteStreamRequest(serverRequest);
        const it = serverRequest.body[Symbol.asyncIterator]();
        const r = await it.next();
        expect(r.done).toBeTrue();
      }
    });
  });
  describe("with HTTP/1.1 ECONNRESET", function () {
    let serverRequest: UniversalServerRequest | undefined;
    const server = useNodeServer(() => {
      serverRequest = undefined;
      const s = http.createServer(
        {
          // We want to test behavior when a connection is dropped, and we do
          // not want to wait for the default check interval of 30 seconds,
          // because it would make our test suite idle for 30 seconds, so we
          // set it to a very low interval.
          //
          // Node 18 also enabled two timeouts by default: headersTimeout and
          // requestTimeout, which are 60s and 5 mins respectively.
          // https://github.com/nodejs/node/pull/41263
          //
          // However, this change seems to be buggy:
          // https://github.com/nodejs/node/issues/44228
          // https://github.com/b3nsn0w/koa-easy-ws/issues/36
          //
          // And coupled with our low check interval, it seems to be causing
          // intermittent timeouts in our test server. So, we are disabling
          // them by default.
          //
          connectionsCheckingInterval: 1,
          requestTimeout: 0,
        },
        function (request) {
          serverRequest = universalRequestFromNodeRequest(request, undefined);
        }
      );
      // For some reason, the type definitions for ServerOptions do not include
      // headersTimeout:
      // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/http.d.ts
      // So, it must be added to the server property after construction.
      s.headersTimeout = 0;
      return s;
    });
    async function request() {
      return new Promise<void>((resolve) => {
        const request = http.request(server.getUrl(), {
          method: "POST",
        });
        request.on("error", () => {
          // we need this event lister so that Node.js does not raise the error
          // we trigger by calling destroy()
        });
        request.flushHeaders();
        setTimeout(() => {
          request.destroy();
          resolve();
        }, 20);
      });
    }
    it("should abort request signal with DubboError and Code.Aborted", async function () {
      await request();
      while (serverRequest?.signal.reason === undefined) {
        await new Promise((r) => setTimeout(r, 1));
      }
      expect(serverRequest.signal.reason).toBeInstanceOf(Error);
      if (serverRequest.signal.reason instanceof Error) {
        expect(serverRequest.signal.reason).toBeInstanceOf(DubboError);
        const ce = DubboError.from(serverRequest.signal.reason);
        expect(ce.message).toBe("[aborted] aborted");
      }
    });
    it("should error request body stream with ECONNRESET", async function () {
      await request();
      expect(serverRequest).toBeDefined();
      if (serverRequest !== undefined) {
        assertByteStreamRequest(serverRequest);
        const it = serverRequest.body[Symbol.asyncIterator]();
        try {
          await it.next();
          fail("expected error");
        } catch (e) {
          expect(e).toBeInstanceOf(Error);
          expect(e).not.toBeInstanceOf(DubboError);
          if (e instanceof Error) {
            expect(e.message).toBe("aborted");
            expect(getNodeErrorProps(e)).toEqual({
              code: "ECONNRESET",
            });
          }
        }
      }
    });
  });
  describe("with HTTP/1.1 request finishing without error", function () {
    let serverRequest: UniversalServerRequest | undefined;
    const server = useNodeServer(() => {
      const s = http.createServer(
        {
          // We want to test behavior when a connection is dropped, and we do
          // not want to wait for the default check interval of 30 seconds,
          // because it would make our test suite idle for 30 seconds, so we
          // set it to a very low interval.
          //
          // Node 18 also enabled two timeouts by default: headersTimeout and
          // requestTimeout, which are 60s and 5 mins respectively.
          // https://github.com/nodejs/node/pull/41263
          //
          // However, this change seems to be buggy:
          // https://github.com/nodejs/node/issues/44228
          // https://github.com/b3nsn0w/koa-easy-ws/issues/36
          //
          // And coupled with our low check interval, it seems to be causing
          // intermittent timeouts in our test server. So, we are disabling
          // them by default.
          //
          connectionsCheckingInterval: 1,
          requestTimeout: 0,
        },
        function (request, response) {
          serverRequest = universalRequestFromNodeRequest(request, undefined);
          response.writeHead(200);
          response.end();
        }
      );
      // For some reason, the type definitions for ServerOptions do not include
      // headersTimeout:
      // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/http.d.ts
      // So, it must be added to the server property after construction.
      s.headersTimeout = 0;
      return s;
    });
    async function request() {
      return new Promise<void>((resolve) => {
        const request = http.request(server.getUrl(), {
          method: "POST",
          // close TCP connection after we're done so that the server shuts down cleanly
          agent: new http.Agent({ keepAlive: false }),
        });
        request.flushHeaders();
        request.end();
        request.on("response", (response) => {
          void readAllBytes(response, Number.MAX_SAFE_INTEGER).then(() =>
            resolve()
          );
        });
      });
    }
    it("should abort request signal with AbortError", async function () {
      await request();
      expect(serverRequest?.signal).toBeInstanceOf(AbortSignal);
      expect(serverRequest?.signal.aborted).toBeTrue();
      expect(serverRequest?.signal.reason).toBeInstanceOf(Error);
      if (serverRequest?.signal.reason instanceof Error) {
        expect(serverRequest.signal.reason.name).toBe("AbortError");
        expect(serverRequest.signal.reason.message).toBe(
          "This operation was aborted"
        );
      }
    });
    it("should silently end request body stream", async function () {
      await request();
      expect(serverRequest).toBeDefined();
      if (serverRequest !== undefined) {
        assertByteStreamRequest(serverRequest);
        const it = serverRequest.body[Symbol.asyncIterator]();
        const r = await it.next();
        expect(r.done).toBeTrue();
      }
    });
  });
});
