%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2015-2026. All Rights Reserved.
%%
%% 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.
%%
%% %CopyrightEnd%
%%
%%
-module(http_test_lib).

-include_lib("common_test/include/ct.hrl").
-include("inets_test_lib.hrl").
-include("http_internal.hrl").
-include("httpc_internal.hrl").

%% Note: This directive should only be used in test suites.
-compile(export_all).
-compile(nowarn_export_all).
-define(SOCKET_BACKLOG, 100).

dummy_server(SocketType, Inet, Extra) ->
    dummy_server(self(), SocketType, Inet, Extra).

dummy_server(Caller, SocketType, Inet, Extra) ->
    Args = [Caller, SocketType, Inet, Extra],
    Pid = spawn(?MODULE, dummy_server_init, Args),
    receive
	{port, Port} ->
	    {Pid, Port}
    end.

dummy_server_init(Caller, ip_comm, Inet, Extra) ->
    ContentCb = proplists:get_value(content_cb, Extra),
    BaseOpts = [binary, {packet, 0}, {reuseaddr,true},
                {active, false}, {nodelay, true}, {backlog, ?SOCKET_BACKLOG}],
    Conf = proplists:get_value(conf, Extra),
    {ok, ListenSocket} = gen_tcp:listen(0, [Inet | BaseOpts]),
    {ok, Port} = inet:port(ListenSocket),
    Caller ! {port, Port},
    dummy_ipcomm_server_loop({httpd_request, parse, [[{max_uri,    ?HTTP_MAX_URI_SIZE},
						      {max_header, ?HTTP_MAX_HEADER_SIZE},
						      {max_version,?HTTP_MAX_VERSION_STRING}, 
						      {max_method, ?HTTP_MAX_METHOD_STRING},
						      {max_content_length, ?HTTP_MAX_CONTENT_LENGTH},
						      {customize, httpd_custom}
						     ]]},
			     [], ContentCb, Conf, ListenSocket);

dummy_server_init(Caller, unix_socket, Inet, Extra) ->
    ContentCb = proplists:get_value(content_cb, Extra),
    UnixSocket = proplists:get_value(unix_socket, Extra),
    SocketAddr = {local, UnixSocket},
    BaseOpts = [binary, {packet, 0}, {reuseaddr,true}, {active, false},
                {nodelay, true}, {ifaddr, SocketAddr}, {backlog, ?SOCKET_BACKLOG}],
    Conf = proplists:get_value(conf, Extra),
    {ok, ListenSocket} = gen_tcp:listen(0, [Inet | BaseOpts]),
    {ok, Port} = inet:port(ListenSocket),
    Caller ! {port, Port},
    dummy_ipcomm_server_loop({httpd_request, parse, [[{max_uri,    ?HTTP_MAX_URI_SIZE},
						      {max_header, ?HTTP_MAX_HEADER_SIZE},
						      {max_version,?HTTP_MAX_VERSION_STRING},
						      {max_method, ?HTTP_MAX_METHOD_STRING},
						      {max_content_length, ?HTTP_MAX_CONTENT_LENGTH},
						      {customize, httpd_custom}
						     ]]},
			     [], ContentCb, Conf, ListenSocket);

dummy_server_init(Caller, ssl, Inet, Extra) ->
    ContentCb = proplists:get_value(content_cb, Extra),
    SSLOptions = proplists:get_value(ssl, Extra),
    Conf = proplists:get_value(conf, Extra),
    BaseOpts = [binary, {active, false}, {nodelay, true},
                {backlog, ?SOCKET_BACKLOG} | SSLOptions],
    dummy_ssl_server_init(Caller, BaseOpts, Inet, ContentCb, Conf).

dummy_ssl_server_init(Caller, BaseOpts, Inet, ContentCb, Conf) ->
    {ok, ListenSocket} = ssl:listen(0, [Inet | BaseOpts]),
    {ok, {_, Port}} = ssl:sockname(ListenSocket),
    Caller ! {port, Port},
    dummy_ssl_server_loop({httpd_request, parse, [[{max_uri,    ?HTTP_MAX_URI_SIZE},
						   {max_method, ?HTTP_MAX_METHOD_STRING},
						   {max_version,?HTTP_MAX_VERSION_STRING}, 
						   {max_method, ?HTTP_MAX_METHOD_STRING},
						   {max_content_length, ?HTTP_MAX_CONTENT_LENGTH},
						   {customize, httpd_custom}
						  ]]},
			  [], ContentCb, Conf, ListenSocket).

dummy_ipcomm_server_loop(MFA, Handlers, ContentCb, Conf, ListenSocket) ->
    receive
	stop ->
	    lists:foreach(fun(Handler) -> Handler ! stop end, Handlers);
	{stop, From} ->
	    Stopper = fun(Handler) -> Handler ! stop end, 
	    lists:foreach(Stopper, Handlers),
	    From ! {stopped, self()}
    after 0 ->
	    {ok, Socket} = gen_tcp:accept(ListenSocket),
	    HandlerPid  = dummy_request_handler(MFA, Socket, ContentCb, Conf),
	    gen_tcp:controlling_process(Socket, HandlerPid),
	    HandlerPid ! ipcomm_controller,
	    dummy_ipcomm_server_loop(MFA, [HandlerPid | Handlers],
				     ContentCb, Conf, ListenSocket)
    end.

dummy_ssl_server_loop(MFA, Handlers, ContentCb, Conf, ListenSocket) ->
    receive
	stop ->
	    lists:foreach(fun(Handler) -> Handler ! stop end, Handlers);
	{stop, From} ->
	    Stopper = fun(Handler) -> Handler ! stop end, 
	    lists:foreach(Stopper, Handlers),
	    From ! {stopped, self()}
    after 0 ->
	    {ok, Socket} = ssl:transport_accept(ListenSocket),
	    HandlerPid  = dummy_request_handler(MFA, Socket, ContentCb, Conf),
	    ssl:controlling_process(Socket, HandlerPid),
	    HandlerPid ! ssl_controller,
	    dummy_ssl_server_loop(MFA, [HandlerPid | Handlers],
				  ContentCb, Conf, ListenSocket)
    end.

dummy_request_handler(MFA, Socket, ContentCb, Conf) ->
    spawn(?MODULE, dummy_request_handler_init, [MFA, Socket, ContentCb, Conf]).

dummy_request_handler_init(MFA, Socket0, ContentCb, Conf) ->
    {SockType, Socket} = 
	receive 
	    ipcomm_controller ->
		inet:setopts(Socket0, [{active, true}]),
		{ip_comm, Socket0};
	    ssl_controller ->
		{ok, Socket1} = ssl:handshake(Socket0, infinity),
		ssl:setopts(Socket0, [{active, true}]),
		{ssl, Socket1}
	end,
    dummy_request_handler_loop(MFA, SockType, Socket, ContentCb, Conf).
    
dummy_request_handler_loop({Module, Function, Args}, SockType, Socket, ContentCb, Conf) ->
    receive 
	{Proto, _, Data} when (Proto =:= tcp) orelse (Proto =:= ssl) ->
	    case handle_request(Module, Function, [Data | Args], SockType,
				Socket, ContentCb, Conf) of
		stop when Proto =:= tcp ->
		    gen_tcp:close(Socket);
		stop when Proto =:= ssl ->
		    ssl:close(Socket);
		NewMFA ->
		    dummy_request_handler_loop(NewMFA, SockType, Socket, ContentCb, Conf)
	    end;
	stop when SockType =:= ip_comm ->
	    gen_tcp:close(Socket);
	stop when SockType =:= ssl ->
	    ssl:close(Socket)
    end.

handle_request(Module, Function, Args, SockType, Socket, ContentCb, Conf) ->
    case Module:Function(Args) of
	{ok, Result} ->
	    case ContentCb:handle_http_msg(Result, Socket, Conf) of
		stop ->
		    stop;
		<<>> ->
		    {httpd_request, parse, [[{max_uri,?HTTP_MAX_URI_SIZE},
					     {max_header, ?HTTP_MAX_HEADER_SIZE},
					     {max_version,?HTTP_MAX_VERSION_STRING}, 
					     {max_method, ?HTTP_MAX_METHOD_STRING},
					     {max_content_length, ?HTTP_MAX_CONTENT_LENGTH},
					     {customize, httpd_custom}
					    ]]};
		Data ->	
		    handle_request(httpd_request, parse, 
				   [Data, [{max_uri,    ?HTTP_MAX_URI_SIZE},
					   {max_header, ?HTTP_MAX_HEADER_SIZE},
					   {max_version,?HTTP_MAX_VERSION_STRING}, 
					   {max_method, ?HTTP_MAX_METHOD_STRING},
					   {max_content_length, ?HTTP_MAX_CONTENT_LENGTH},
					   {customize, httpd_custom}
					  ]], SockType, Socket, ContentCb, Conf)
	    end;

	%% The request was rejected by the parser. Mirror what
	%% httpd_request_handler does and send a status response, then
	%% close. Without this the error tuple is indistinguishable from
	%% an {M, F, A} continuation, so the handler would silently wait
	%% for more data and the client would hang until its timetrap
	%% expires instead of failing on the unexpected response.
	{error, {size_error, _MaxSize, StatusCode, Description}, Version} ->
	    send_status(SockType, Socket, Version, StatusCode, Description),
	    stop;
	{error, {version_error, StatusCode, Description}, Version} ->
	    send_status(SockType, Socket, Version, StatusCode, Description),
	    stop;
	{error, {bad_request, StatusCode, Description}, Version}
	  when is_integer(StatusCode) ->
	    send_status(SockType, Socket, Version, StatusCode, Description),
	    stop;
	{error, Reason, Version} ->
	    ct:log("Dummy server rejected request: ~p", [Reason]),
	    send_status(SockType, Socket, Version, 400, "Bad Request"),
	    stop;
	{error, Reason} ->
	    ct:log("Dummy server rejected request: ~p", [Reason]),
	    send_status(SockType, Socket, "HTTP/1.1", 400, "Bad Request"),
	    stop;

	NewMFA ->
	    NewMFA
    end.

send_status(SockType, Socket, Version, StatusCode, Description) ->
    Phrase = httpd_util:reason_phrase(StatusCode),
    Body = "<HTML><BODY><H1>" ++ integer_to_list(StatusCode) ++ " " ++ Phrase
	++ "</H1>" ++ Description ++ "</BODY></HTML>",
    Response = Version ++ " " ++ integer_to_list(StatusCode) ++ " " ++ Phrase
	++ "\r\nContent-Type: text/html"
	++ "\r\nContent-Length: " ++ integer_to_list(length(Body))
	++ "\r\nConnection: close\r\n\r\n" ++ Body,
    inets_test_lib:send(SockType, Socket, Response).

%% Perform a synchronous stop
dummy_server_stop(Pid) ->
    Pid ! {stop, self()},
    receive 
	{stopped, Pid} ->
	    ok
    end.

open_big_data_socket(gen_tcp, Pid, _Config) ->
    {ok, LSock} = gen_tcp:listen(0, [binary, {packet, 0},
                                        {active, false}]),
    {ok, Port} = inet:port(LSock),
    Pid ! {started_listen, Port},
    (fun F() ->
        receive
            {stop_listen, CleanupPid} ->
                gen_tcp:close(LSock),
                CleanupPid ! stopped_listen
        after 0 ->
            accept_and_send_data(gen_tcp, LSock),
            F()
        end
    end)();

open_big_data_socket(ssl, Pid, Config) ->
    SSLOptions =  Config ++ [{verify, verify_none}, {reuseaddr, true}, binary, {packet, 0}, {active, false}],
    {ok, LSock} = ssl:listen(0, SSLOptions),
    {ok, {_, Port}} = ssl:sockname(LSock),
    Pid ! {started_listen, Port},
    (fun F() ->
        receive
            {stop_listen, CleanupPid} ->
                ssl:close(LSock),
                CleanupPid ! stopped_listen
        after 0 ->
            accept_and_send_data(ssl, LSock),
            F()
        end
    end)().

accept_and_send_data(gen_tcp, Sock) ->
    {ok, Sock1} = gen_tcp:accept(Sock),
    F = fun() ->
        receive start -> ok end,
        gen_tcp:recv(Sock1, 0),
        gen_tcp:send(Sock1, ["HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"]),
        ChunkSize = 1024*64*8, % 64KB
        << begin gen_tcp:send(Sock1, <<Chunk:ChunkSize>>), <<>> end || <<Chunk:ChunkSize>> <= ?DATA_20MB>>,
        receive after 10000 -> ok end,
        gen_tcp:close(Sock1)
    end,
    HandlerPid = spawn(F),
    gen_tcp:controlling_process(Sock1, HandlerPid),
    HandlerPid ! start;
accept_and_send_data(ssl, LSock) ->
    {ok, Sock1} = ssl:transport_accept(LSock),
    F = fun() ->
        receive start -> ok end,
        {ok, Sock} = ssl:handshake(Sock1),
        ssl:recv(Sock, 0),
        ssl:send(Sock, ["HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"]),
        ChunkSize = 1024*64*8, % 64KB
        << begin ssl:send(Sock, <<Chunk:ChunkSize>>), <<>> end || <<Chunk:ChunkSize>> <= ?DATA_20MB>>,
        receive after 5000 -> ok end,
        ssl:close(Sock)
     end,
    HandlerPid = spawn(F),
    ssl:controlling_process(Sock1, HandlerPid),
    HandlerPid ! start.
