%% Copyright (c) Björn Svensson <bjorn.a.svensson@est.tech>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

-module(send_errors_SUITE).
-compile(export_all).
-compile(nowarn_export_all).

-import(ct_helper, [doc/1]).
-import(gun_test, [http2_handshake/2]).

suite() ->
	[{timetrap, 180000}].

all() ->
	[{group, gun}].

groups() ->
	[{gun, [parallel], ct_helper:all(?MODULE)}].

init_per_suite(Config) ->
	case os:type() of
		{_, linux} -> Config;
		_ -> {skip, "This test suite is Linux-only due to socket juggling."}
	end.

end_per_suite(_) -> ok.

%% Tests.

http2_send_request_fail(_) ->
	doc("Handle send failures of requests in HTTP/2."),
	{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
	{ok, {_, Port}} = inet:sockname(ListenSocket),
	%% Socket buffers needs to be smaller than local_window/ConnWindow
	{ok, Pid} = gun:open("localhost", Port, #{
		protocols => [http2],
		tcp_opts => [
			{send_timeout, 250},
			{send_timeout_close, true},
			{sndbuf, 2048},
			{nodelay, true}
		]
	}),
	{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000),
	inet:setopts(ClientSocket, [{recbuf, 512}]),
	http2_handshake(ClientSocket, gen_tcp),
	{ok, http2} = gun:await_up(Pid),
	post_loop(Pid, 1000), %% Fill buffer
	receive
		{gun_error, Pid, _, {closed, {error, _}}} ->
			gun:close(Pid);
		Msg ->
			error({fail, Msg})
	after 5000 ->
		error(timeout)
	end.

http2_send_ping_fail(_) ->
	doc("Handle send failures of ping in HTTP/2."),
	{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
	{ok, {_, Port}} = inet:sockname(ListenSocket),
	{ok, Pid} = gun:open("localhost", Port, #{
		protocols => [http2],
		http2_opts => #{keepalive => 1},
		tcp_opts => [
			{send_timeout, 250},
			{send_timeout_close, true},
			{sndbuf, 256},
			{nodelay, true}
		]
	}),
	{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000),
	inet:setopts(ClientSocket, [{recbuf, 256}]),
	http2_handshake(ClientSocket, gen_tcp),
	{ok, http2} = gun:await_up(Pid),
	receive
		{gun_down, Pid, http2, {error, _}, []} ->
			gun:close(Pid);
		Msg ->
			error({fail, Msg})
	after 15000 ->
		error(timeout)
	end.

http2_send_ping_ack_fail(_) ->
	doc("Handle send failures of ping ack in HTTP/2."),
	{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
	{ok, {_, Port}} = inet:sockname(ListenSocket),
	{ok, Pid} = gun:open("localhost", Port, #{
		protocols => [http2],
		http2_opts => #{keepalive => infinity},
		tcp_opts => [
			{send_timeout, 250},
			{send_timeout_close, true},
			{sndbuf, 256},
			{nodelay, true}
		]
	}),
	{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000),
	inet:setopts(ClientSocket, [{recbuf, 256}]),
	http2_handshake(ClientSocket, gen_tcp),
	{ok, http2} = gun:await_up(Pid),
	ping_loop(ClientSocket, 5000), %% Send pings triggering ping acks
	receive
		{gun_down, Pid, http2, {error, _}, []} ->
			gun:close(Pid);
		Msg ->
			error({fail, Msg})
	after 15000 ->
		error(timeout)
	end.

%% Helpers

post_loop(_Pid, 0) ->
	ok;
post_loop(Pid, Loops) ->
	Body = <<0:1000>>,
	gun:post(Pid, "/organizations/ninenines",
		[{<<"content-type">>, "application/octet-stream"}],
		Body),
	post_loop(Pid, Loops - 1).

ping_loop(_Socket, 0) ->
	ok;
ping_loop(Socket, Loops) ->
	gun_tcp:send(Socket, cow_http2:ping(0)),
	ping_loop(Socket, Loops - 1).
