package com.cameleer.server.app.alerting.notify;

import com.cameleer.server.app.alerting.dto.UnreadCountResponse;
import com.cameleer.server.core.alerting.AlertInstance;
import com.cameleer.server.core.alerting.AlertInstanceRepository;
import com.cameleer.server.core.alerting.AlertSeverity;
import com.cameleer.server.core.rbac.GroupSummary;
import com.cameleer.server.core.rbac.RbacService;
import com.cameleer.server.core.rbac.RoleSummary;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;

import org.mockito.ArgumentMatchers;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * Unit test for {@link InAppInboxQuery}.
 * <p>
 * Uses a controllable {@link Clock} to test the 5-second memoization of
 * {@link InAppInboxQuery#countUnread}.
 */
@ExtendWith(MockitoExtension.class)
class InAppInboxQueryTest {

    @Mock private AlertInstanceRepository instanceRepo;
    @Mock private RbacService rbacService;

    /** Tick-able clock: each call to millis() returns the current value of this field. */
    private final AtomicLong nowMillis = new AtomicLong(1_000_000L);

    private Clock tickableClock;
    private InAppInboxQuery query;

    private static final UUID ENV_ID  = UUID.randomUUID();
    private static final String USER_ID = "user-123";

    @BeforeEach
    void setUp() {
        tickableClock = new Clock() {
            @Override public ZoneOffset getZone() { return ZoneOffset.UTC; }
            @Override public Clock withZone(java.time.ZoneId zone) { return this; }
            @Override public Instant instant() { return Instant.ofEpochMilli(nowMillis.get()); }
        };

        query = new InAppInboxQuery(instanceRepo, rbacService, tickableClock);

        lenient().when(rbacService.getEffectiveGroupsForUser(anyString())).thenReturn(List.of());
        lenient().when(rbacService.getEffectiveRolesForUser(anyString())).thenReturn(List.of());
    }

    // -------------------------------------------------------------------------
    // listInbox
    // -------------------------------------------------------------------------

    @Test
    void listInbox_delegatesWithResolvedGroupsAndRoles() {
        UUID groupId = UUID.randomUUID();
        UUID roleId  = UUID.randomUUID();
        when(rbacService.getEffectiveGroupsForUser(USER_ID))
                .thenReturn(List.of(new GroupSummary(groupId, "ops-group")));
        when(rbacService.getEffectiveRolesForUser(USER_ID))
                .thenReturn(List.of(new RoleSummary(roleId, "OPERATOR", true, "direct")));

        when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of(groupId.toString())),
                eq(USER_ID), eq(List.of("OPERATOR")), isNull(), isNull(), isNull(), isNull(), eq(20)))
                .thenReturn(List.of());

        List<AlertInstance> result = query.listInbox(ENV_ID, USER_ID, null, null, null, null, 20);
        assertThat(result).isEmpty();
        verify(instanceRepo).listForInbox(ENV_ID, List.of(groupId.toString()),
                USER_ID, List.of("OPERATOR"), null, null, null, null, 20);
    }

    @Test
    void listInbox_forwardsStateAndSeverityFilters() {
        when(rbacService.getEffectiveGroupsForUser(USER_ID)).thenReturn(List.of());
        when(rbacService.getEffectiveRolesForUser(USER_ID)).thenReturn(List.of());

        List<com.cameleer.server.core.alerting.AlertState> states =
                List.of(com.cameleer.server.core.alerting.AlertState.FIRING);
        List<AlertSeverity> severities = List.of(AlertSeverity.CRITICAL, AlertSeverity.WARNING);

        when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of()), eq(USER_ID), eq(List.of()),
                eq(states), eq(severities), isNull(), isNull(), eq(25)))
                .thenReturn(List.of());

        query.listInbox(ENV_ID, USER_ID, states, severities, null, null, 25);
        verify(instanceRepo).listForInbox(ENV_ID, List.of(), USER_ID, List.of(),
                states, severities, null, null, 25);
    }

    // -------------------------------------------------------------------------
    // countUnread — bySeverity shape
    // -------------------------------------------------------------------------

    @Test
    void countUnread_totalIsSumOfBySeverityValues() {
        when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(severities(4L, 2L, 1L));

        UnreadCountResponse response = query.countUnread(ENV_ID, USER_ID);

        assertThat(response.total()).isEqualTo(7L);
        assertThat(response.bySeverity())
                .containsEntry(AlertSeverity.CRITICAL, 4L)
                .containsEntry(AlertSeverity.WARNING,  2L)
                .containsEntry(AlertSeverity.INFO,     1L);
    }

    @Test
    void countUnread_fillsMissingSeveritiesWithZero() {
        // Repository returns only CRITICAL — WARNING/INFO must default to 0.
        when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(Map.of(AlertSeverity.CRITICAL, 3L));

        UnreadCountResponse response = query.countUnread(ENV_ID, USER_ID);

        assertThat(response.total()).isEqualTo(3L);
        assertThat(response.bySeverity())
                .containsEntry(AlertSeverity.CRITICAL, 3L)
                .containsEntry(AlertSeverity.WARNING,  0L)
                .containsEntry(AlertSeverity.INFO,     0L);
    }

    // -------------------------------------------------------------------------
    // countUnread — memoization
    // -------------------------------------------------------------------------

    @Test
    void countUnread_secondCallWithin5sUsesCache() {
        when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(severities(1L, 2L, 2L));

        UnreadCountResponse first  = query.countUnread(ENV_ID, USER_ID);
        nowMillis.addAndGet(4_000L);
        UnreadCountResponse second = query.countUnread(ENV_ID, USER_ID);

        assertThat(first.total()).isEqualTo(5L);
        assertThat(second.total()).isEqualTo(5L);
        verify(instanceRepo, times(1)).countUnreadBySeverity(eq(ENV_ID), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any());
    }

    @Test
    void countUnread_callAfter5sRefreshesCache() {
        when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(severities(1L, 1L, 1L))   // first call — total 3
                .thenReturn(severities(4L, 3L, 2L));  // after TTL  — total 9

        UnreadCountResponse first = query.countUnread(ENV_ID, USER_ID);
        nowMillis.addAndGet(5_001L);
        UnreadCountResponse third = query.countUnread(ENV_ID, USER_ID);

        assertThat(first.total()).isEqualTo(3L);
        assertThat(third.total()).isEqualTo(9L);
        verify(instanceRepo, times(2)).countUnreadBySeverity(eq(ENV_ID), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any());
    }

    @Test
    void countUnread_differentUsersDontShareCache() {
        when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq("alice"),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(severities(0L, 1L, 1L));
        when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq("bob"),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(severities(2L, 2L, 4L));

        assertThat(query.countUnread(ENV_ID, "alice").total()).isEqualTo(2L);
        assertThat(query.countUnread(ENV_ID, "bob").total()).isEqualTo(8L);
        verify(instanceRepo).countUnreadBySeverity(eq(ENV_ID), eq("alice"),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any());
        verify(instanceRepo).countUnreadBySeverity(eq(ENV_ID), eq("bob"),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any());
    }

    @Test
    void countUnread_differentEnvsDontShareCache() {
        UUID envA = UUID.randomUUID();
        UUID envB = UUID.randomUUID();
        when(instanceRepo.countUnreadBySeverity(eq(envA), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(severities(0L, 0L, 1L));
        when(instanceRepo.countUnreadBySeverity(eq(envB), eq(USER_ID),
                ArgumentMatchers.<List<String>>any(), ArgumentMatchers.<List<String>>any()))
                .thenReturn(severities(1L, 1L, 2L));

        assertThat(query.countUnread(envA, USER_ID).total()).isEqualTo(1L);
        assertThat(query.countUnread(envB, USER_ID).total()).isEqualTo(4L);
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private static Map<AlertSeverity, Long> severities(long critical, long warning, long info) {
        EnumMap<AlertSeverity, Long> m = new EnumMap<>(AlertSeverity.class);
        m.put(AlertSeverity.CRITICAL, critical);
        m.put(AlertSeverity.WARNING,  warning);
        m.put(AlertSeverity.INFO,     info);
        return m;
    }
}
