-module(stats_collector_tests). -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). -define(TABLES, [user, event, calendar, booking, review, report, ticket, subscription, stats_counter, stats_daily]). setup() -> eh_test_support:start_mnesia(), eh_test_support:ensure_tables(?TABLES), {ok, _} = stats_collector:start_link(), ok = stats_collector:subscribe(), ok. cleanup(_) -> case whereis(stats_collector) of undefined -> ok; Pid -> unlink(Pid), exit(Pid, kill), wait_dead(Pid, 50) end, eh_test_support:delete_tables(?TABLES), eh_test_support:stop_mnesia(), ok. wait_dead(_Pid, 0) -> ok; wait_dead(Pid, N) -> case is_process_alive(Pid) of true -> timer:sleep(20), wait_dead(Pid, N - 1); false -> ok end. stats_collector_test_() -> {foreach, fun setup/0, fun cleanup/1, [ {"backfill from existing rows", fun test_backfill/0}, {"live write updates dims and daily", fun test_live_write/0}, {"status transition moves counters", fun test_status_move/0}, {"core_stats reads via collector", fun test_core_stats_api/0}, {"ticket avg resolution from counters", fun test_ticket_avg_resolution/0}, {"report avg resolution from counters", fun test_report_avg_resolution/0} ]}. test_backfill() -> Now = calendar:universal_time(), ok = mnesia:dirty_write(eh_test_support:make_user(#{ id => <<"u1">>, status => pending, role => user, created_at => Now })), ok = mnesia:dirty_write(eh_test_support:make_user(#{ id => <<"u2">>, status => active, role => user, created_at => Now })), ok = stats_collector:rebuild(), ?assertEqual(1, core_stats:with_counter({user_by_status, pending}, fun() -> -1 end)), ?assertEqual(1, core_stats:with_counter({user_by_status, active}, fun() -> -1 end)), {Date, _} = Now, From = {Date, {0, 0, 0}}, To = {Date, {23, 59, 59}}, Daily = core_stats:with_daily(users_created, From, To, fun() -> [] end), ?assertEqual([{Date, 2}], Daily). test_live_write() -> Now = calendar:universal_time(), ok = mnesia:dirty_write(eh_test_support:make_user(#{ id => <<"live1">>, status => active, created_at => Now })), wait_counter({user_by_status, active}, 1, 20), ?assertEqual(1, core_stats:with_counter({user_by_status, active}, fun() -> 0 end)). test_status_move() -> Now = calendar:universal_time(), User = eh_test_support:make_user(#{ id => <<"move1">>, status => pending, created_at => Now }), ok = mnesia:dirty_write(User), wait_counter({user_by_status, pending}, 1, 20), ok = mnesia:dirty_write(User#user{status = active}), wait_counter({user_by_status, pending}, 0, 20), wait_counter({user_by_status, active}, 1, 20), ?assertEqual(0, core_stats:with_counter({user_by_status, pending}, fun() -> -1 end)), ?assertEqual(1, core_stats:with_counter({user_by_status, active}, fun() -> -1 end)). test_core_stats_api() -> ?assert(core_stats:available()), Now = calendar:universal_time(), ok = mnesia:dirty_write(#report{ id = <<"r1">>, reporter_id = <<"u1">>, target_type = calendar, target_id = <<"c1">>, reason = <<"spam">>, status = pending, created_at = Now, resolved_at = undefined, resolved_by = <<>> }), wait_counter({report_by_status, pending}, 1, 20), {ok, Dim} = core_stats:get_dim(report, status), ?assertEqual(1, proplists:get_value(pending, Dim, 0)). test_ticket_avg_resolution() -> First = {{2026, 1, 1}, {0, 0, 0}}, Closed = {{2026, 1, 1}, {2, 0, 0}}, %% 2 hours ok = mnesia:dirty_write(#ticket{ id = <<"t_avg">>, reporter_id = <<"u1">>, error_hash = <<"h">>, error_message = <<"e">>, stacktrace = <<>>, context = <<>>, count = 1, first_seen = First, last_seen = Closed, status = closed, assigned_to = <<>>, resolution_note = <<>>, closed_at = Closed, source = <<"backend">> }), wait_counter(ticket_resolved_count, 1, 20), Avg = core_ticket:avg_resolution_time(), ?assertEqual(2.0, Avg). test_report_avg_resolution() -> Created = {{2026, 1, 1}, {0, 0, 0}}, Resolved = {{2026, 1, 1}, {3, 0, 0}}, %% 3 hours ok = mnesia:dirty_write(#report{ id = <<"r_avg">>, reporter_id = <<"u1">>, target_type = calendar, target_id = <<"c1">>, reason = <<"spam">>, status = reviewed, created_at = Created, resolved_at = Resolved, resolved_by = <<"adm">> }), wait_counter({report_resolved_count, reviewed}, 1, 20), Avg = core_report:avg_resolution_time(reviewed), ?assertEqual(3.0, Avg). wait_counter(Key, Expected, 0) -> Actual = core_stats:with_counter(Key, fun() -> -999 end), ?assertEqual(Expected, Actual); wait_counter(Key, Expected, N) -> case core_stats:with_counter(Key, fun() -> -999 end) of Expected -> ok; _ -> timer:sleep(25), wait_counter(Key, Expected, N - 1) end.