%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2012-2025. 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(erl2html2_SUITE).

-compile(export_all).

-include_lib("common_test/include/ct.hrl").


-define(HEADER,
	["<!DOCTYPE HTML PUBLIC",
	 "\"-//W3C//DTD HTML 3.2 Final//EN\">\n",
	 "<!-- autogenerated by 'erl2html2' -->\n",
	 "<html>\n",
	 "<head><title>Module ", Src, "</title>\n",
	 "<meta http-equiv=\"cache-control\" ",
	 "content=\"no-cache\"></meta>\n",
	 "</head>\n",
	 "<body bgcolor=\"white\" text=\"black\" ",
	 "link=\"blue\" vlink=\"purple\" alink=\"red\">\n"]).

%%--------------------------------------------------------------------
%% @spec suite() -> Info
%% Info = [tuple()]
%% @end
%%--------------------------------------------------------------------
suite() ->
    [{timetrap,{seconds,30}},
     {ct_hooks,[ts_install_cth,test_server_test_lib]}].

%%--------------------------------------------------------------------
%% @spec init_per_suite(Config0) ->
%%     Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
%% Config0 = Config1 = [tuple()]
%% Reason = term()
%% @end
%%--------------------------------------------------------------------
init_per_suite(Config) ->
    Config.

%%--------------------------------------------------------------------
%% @spec end_per_suite(Config0) -> void() | {save_config,Config1}
%% Config0 = Config1 = [tuple()]
%% @end
%%--------------------------------------------------------------------
end_per_suite(_Config) ->
    ok.

%%--------------------------------------------------------------------
%% @spec init_per_group(GroupName, Config0) ->
%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
%% GroupName = atom()
%% Config0 = Config1 = [tuple()]
%% Reason = term()
%% @end
%%--------------------------------------------------------------------
init_per_group(_GroupName, Config) ->
    Config.

%%--------------------------------------------------------------------
%% @spec end_per_group(GroupName, Config0) ->
%%               void() | {save_config,Config1}
%% GroupName = atom()
%% Config0 = Config1 = [tuple()]
%% @end
%%--------------------------------------------------------------------
end_per_group(_GroupName, _Config) ->
    ok.

%%--------------------------------------------------------------------
%% @spec init_per_testcase(TestCase, Config0) ->
%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
%% TestCase = atom()
%% Config0 = Config1 = [tuple()]
%% Reason = term()
%% @end
%%--------------------------------------------------------------------
init_per_testcase(_TestCase, Config) ->
    Config.

%%--------------------------------------------------------------------
%% @spec end_per_testcase(TestCase, Config0) ->
%%               void() | {save_config,Config1} | {fail,Reason}
%% TestCase = atom()
%% Config0 = Config1 = [tuple()]
%% Reason = term()
%% @end
%%--------------------------------------------------------------------
end_per_testcase(_TestCase, _Config) ->
    ok.

%%--------------------------------------------------------------------
%% @spec groups() -> [Group]
%% Group = {GroupName,Properties,GroupsAndTestCases}
%% GroupName = atom()
%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}]
%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase]
%% TestCase = atom()
%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}}
%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail |
%%              repeat_until_any_ok | repeat_until_any_fail
%% N = integer() | forever
%% @end
%%--------------------------------------------------------------------
groups() ->
    [].

%%--------------------------------------------------------------------
%% @spec all() -> GroupsAndTestCases | {skip,Reason}
%% GroupsAndTestCases = [{group,GroupName} | TestCase]
%% GroupName = atom()
%% TestCase = atom()
%% Reason = term()
%% @end
%%--------------------------------------------------------------------
all() ->
    [macros_defined, macros_undefined].

%%--------------------------------------------------------------------
%% @spec TestCase(Config0) ->
%%               ok | exit() | {skip,Reason} | {comment,Comment} |
%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
%% Config0 = Config1 = [tuple()]
%% Reason = term()
%% Comment = term()
%% @end
%%--------------------------------------------------------------------
macros_defined(Config) ->
    %% let erl2html2 use epp as parser
    DataDir = ?config(data_dir,Config),
    InclDir = filename:join(DataDir, "include"),
    {Src,Dst} = convert_module("m1",[InclDir],Config),
    {true,L} = check_line_numbers(Src,Dst),
    ok = check_link_targets(Src,Dst,L,[{baz,0}],[]),
    ok.

macros_undefined(Config) ->
    %% let erl2html2 use epp_dodger as parser
    {Src,Dst} = convert_module("m1",[],Config),
    {true,L} = check_line_numbers(Src,Dst),
    ok = check_link_targets(Src,Dst,L,[{baz,0}],[{quux,0}]),
    ok.

convert_module(Mod,InclDirs,Config) ->
    DataDir = ?config(data_dir,Config),
    PrivDir = ?config(priv_dir,Config),
    Src = filename:join(DataDir,Mod++".erl"),
    Dst = filename:join(PrivDir,Mod++".erl.html"),
    io:format("<a href=\"~ts\">~s</a>\n",[Src,filename:basename(Src)]),
    ok = erl2html2:convert(Src, Dst, InclDirs, "<html><body>"),
    io:format("<a href=\"~ts\">~s</a>\n",[Dst,filename:basename(Dst)]),
    {Src,Dst}.

%% Check that there are the same number of lines in each file, and
%% that all line numbers are displayed in the dst file.
check_line_numbers(Src,Dst) ->
    {ok,SFd} = file:open(Src,[read]),
    {ok,DFd} = file:open(Dst,[read]),
    {ok,SN} = count_src_lines(SFd,0),
    ok = file:close(SFd),
    {ok,DN} = read_dst_line_numbers(DFd),
    ok = file:close(DFd),
    {SN == DN,SN}.

count_src_lines(Fd,N) ->
    case io:get_line(Fd,"") of
	eof ->
	    {ok,N};
	{error,Reason} ->
	    {error,Reason,N};
	_Line ->
	    count_src_lines(Fd,N+1)
    end.

read_dst_line_numbers(Fd) ->
    "<html><body><pre>\n" = io:get_line(Fd,""),
    read_dst_line_numbers(Fd,0).
read_dst_line_numbers(Fd,Last) when is_integer(Last) ->
    case io:get_line(Fd,"") of
	eof ->
	    {ok,Last};
	{error,Reason} ->
	    {error,Reason,Last};
	"</pre>"++_ ->
	    {ok,Last};
	"</body>"++_ ->
	    {ok,Last};
	Line ->
	    %% erlang:display(Line),
	    Num = check_line_number(Last,Line,Line),
	    read_dst_line_numbers(Fd,Num)
    end.

check_line_number(Last,Line,OrigLine) ->
    case Line of
	"<a name="++_ ->
	    [$>|Rest] = lists:dropwhile(fun($>) -> false; (_) -> true end,Line),
	    check_line_number(Last,Rest,OrigLine);
	_ ->
	    [N |_] = string:lexemes(Line,":"),
%	    erlang:display(N),
	    Num =
		try list_to_integer(string:trim(N,both,"\s"))
		catch _:_ -> ct:fail({no_line_number_after,Last,OrigLine})
		end,
	    if Num == Last+1 ->
		    Num;
	       true ->
		    ct:fail({unexpected_integer,Num,Last})
	    end
    end.


%% Check that there is one link target for each line and one for each
%% function.
%% The test module has -compile(export_all), so all functions are
%% found by listing the exported ones.
check_link_targets(Src,Dst,L,RmFncs,ShouldRemain) ->
    Mod = list_to_atom(filename:basename(filename:rootname(Src))),
    Exports = Mod:module_info(exports)--[{module_info,0},{module_info,1}|RmFncs],
    LastExprFuncs = [Func || {Func,_A} <- Exports],
    {ok,{FAs,Fs,L},_} =
	xmerl_sax_parser:file(Dst,
			      [{event_fun,fun sax_event/3},
			       {event_state,{Exports,LastExprFuncs,0}}]),    
    true = (length(FAs) == length(ShouldRemain)),
    [] = [FA || FA <- FAs, not lists:member(FA,ShouldRemain)],
    [] = [F || F <- Fs, not lists:keymember(F,1,ShouldRemain)],
    ok.

sax_event(Event,_Loc,State) ->
    sax_event(Event,State).

sax_event({startElement,_Uri,"a",_QN,Attrs},{Exports,LastExprFuncs,PrevLine}) ->
    {_,_,"name",Name} = lists:keyfind("name",3,Attrs),
    case catch list_to_integer(Name) of
	Line when is_integer(Line) ->
	    case PrevLine + 1 of
		Line ->
		    {Exports,LastExprFuncs,Line};
		Other ->
		    ct:fail({unexpected_line_number_target,Other})
	    end;
	{'EXIT',_} ->
	    {match,[FStr,EndStr]} =
		 re:run(Name,"^(.*)-(last_expr|[0-9]+)$",
			[{capture,all_but_first,list}]),
	    F = list_to_atom(uri_string:unquote(FStr)),
	    case EndStr of
		"last_expr" ->
		    true = lists:member(F,LastExprFuncs),
		    {Exports,lists:delete(F,LastExprFuncs),PrevLine};
		_ ->
		    A = list_to_integer(EndStr),
		    A = proplists:get_value(F,Exports),
		    {lists:delete({F,A},Exports),LastExprFuncs,PrevLine}
	    end
    end;
sax_event(_,State) ->
    State.
