package com.pandoli365.bibimbap.service;

import com.pandoli365.bibimbap.badge.BadgeService;
import com.pandoli365.bibimbap.data.UserData;
import com.pandoli365.bibimbap.mapper.ReputationEventsMapper;
import com.pandoli365.bibimbap.mapper.UserBadgesMapper;
import com.pandoli365.bibimbap.mapper.UsersMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class BadgeServiceTest {

    private static final long USER_ID = 7L;
    private static final long AWARDED_BY = 99L;

    @Mock
    private ReputationEventsMapper reputationEventsMapper;
    @Mock
    private UserBadgesMapper userBadgesMapper;
    @Mock
    private UsersMapper usersMapper;

    private BadgeService service() {
        return new BadgeService(reputationEventsMapper, userBadgesMapper, usersMapper);
    }

    // ---- VP-1 / AC-3: 자동부여(임계 도달 시 멱등 부여) ----

    @Test
    void evaluateAndSyncGrantsWhenThresholdReached() {
        // REVIEWER threshold=10, eventType=REVIEW_WRITTEN. 10 >= 10 → 부여.
        when(reputationEventsMapper.countActive(USER_ID, "REVIEW_WRITTEN")).thenReturn(10L);

        service().evaluateAndSync(USER_ID, "REVIEWER");

        verify(userBadgesMapper).insertIgnore(USER_ID, "REVIEWER");
    }

    @Test
    void evaluateAndSyncSkipsWhenBelowThreshold() {
        // 9 < 10 → 미부여.
        when(reputationEventsMapper.countActive(USER_ID, "REVIEW_WRITTEN")).thenReturn(9L);

        service().evaluateAndSync(USER_ID, "REVIEWER");

        verify(userBadgesMapper, never()).insertIgnore(USER_ID, "REVIEWER");
    }

    @Test
    void evaluateAndSyncIsIdempotentAcrossRepeatedCalls() {
        // ux_user_badges_active 부분유니크(WHERE revoked_at IS NULL)가 DB 차원에서 활성 1행을 보장한다.
        // L1 단위 테스트는 호출 위임만 검증한다: 2회 평가 시 insertIgnore 가 2회 호출되더라도
        // insertIgnore 자체가 ON CONFLICT DO NOTHING(0행 반환=이미 보유) 이라 부수효과는 누적되지 않는다.
        when(reputationEventsMapper.countActive(USER_ID, "REVIEW_WRITTEN")).thenReturn(12L);
        when(userBadgesMapper.insertIgnore(USER_ID, "REVIEWER")).thenReturn(1).thenReturn(0);

        BadgeService service = service();
        service.evaluateAndSync(USER_ID, "REVIEWER");
        service.evaluateAndSync(USER_ID, "REVIEWER");

        verify(userBadgesMapper, times(2)).insertIgnore(USER_ID, "REVIEWER");
    }

    @Test
    void evaluateAndSyncGrantsTechnicianWhenThresholdReached() {
        // TECHNICIAN threshold=3, eventType=GAME_UPLOADED. 3 >= 3 → 부여.
        when(reputationEventsMapper.countActive(USER_ID, "GAME_UPLOADED")).thenReturn(3L);

        service().evaluateAndSync(USER_ID, "TECHNICIAN");

        verify(userBadgesMapper).insertIgnore(USER_ID, "TECHNICIAN");
    }

    // ---- grantManual 분기 ----

    @Test
    void grantManualGrantsForValidNewBadge() {
        when(usersMapper.getUser(USER_ID)).thenReturn(new UserData());
        when(userBadgesMapper.existsActive(USER_ID, "REVIEWER")).thenReturn(false);

        BadgeService.GrantResult result = service().grantManual(USER_ID, "REVIEWER", AWARDED_BY);

        assertThat(result).isEqualTo(BadgeService.GrantResult.GRANTED);
        verify(userBadgesMapper).insert(USER_ID, "REVIEWER", AWARDED_BY);
    }

    @Test
    void grantManualReturnsAlreadyActiveWhenHeld() {
        when(usersMapper.getUser(USER_ID)).thenReturn(new UserData());
        when(userBadgesMapper.existsActive(USER_ID, "REVIEWER")).thenReturn(true);

        BadgeService.GrantResult result = service().grantManual(USER_ID, "REVIEWER", AWARDED_BY);

        assertThat(result).isEqualTo(BadgeService.GrantResult.ALREADY_ACTIVE);
        verify(userBadgesMapper, never()).insert(USER_ID, "REVIEWER", AWARDED_BY);
    }

    @Test
    void grantManualReturnsUserNotFoundWhenUserMissing() {
        when(usersMapper.getUser(USER_ID)).thenReturn(null);

        BadgeService.GrantResult result = service().grantManual(USER_ID, "REVIEWER", AWARDED_BY);

        assertThat(result).isEqualTo(BadgeService.GrantResult.USER_NOT_FOUND);
        verify(userBadgesMapper, never()).insert(USER_ID, "REVIEWER", AWARDED_BY);
    }

    @Test
    void grantManualReturnsInvalidBadgeForUnknownKey() {
        // 무효 키는 유저 조회 전에 즉시 차단 → usersMapper/userBadgesMapper 미접촉.
        BadgeService.GrantResult result = service().grantManual(USER_ID, "FOO", AWARDED_BY);

        assertThat(result).isEqualTo(BadgeService.GrantResult.INVALID_BADGE);
        verify(userBadgesMapper, never()).insert(USER_ID, "FOO", AWARDED_BY);
    }

    // ---- revokeManual 분기(AC-6/AC-7) ----

    @Test
    void revokeManualReturnsRevokedWhenRowAffected() {
        when(userBadgesMapper.revoke(USER_ID, "REVIEWER", "정책 위반")).thenReturn(1);

        BadgeService.RevokeResult result = service().revokeManual(USER_ID, "REVIEWER", "정책 위반");

        assertThat(result).isEqualTo(BadgeService.RevokeResult.REVOKED);
    }

    @Test
    void revokeManualReturnsNotActiveWhenNoRowAffected() {
        // 활성 미보유 → UPDATE ... WHERE revoked_at IS NULL 가 0행.
        when(userBadgesMapper.revoke(USER_ID, "REVIEWER", "사유")).thenReturn(0);

        BadgeService.RevokeResult result = service().revokeManual(USER_ID, "REVIEWER", "사유");

        assertThat(result).isEqualTo(BadgeService.RevokeResult.NOT_ACTIVE);
    }

    @Test
    void revokeManualReturnsInvalidBadgeForUnknownKey() {
        // 무효 키는 revoke 호출 전에 차단.
        BadgeService.RevokeResult result = service().revokeManual(USER_ID, "FOO", "사유");

        assertThat(result).isEqualTo(BadgeService.RevokeResult.INVALID_BADGE);
        verify(userBadgesMapper, never()).revoke(USER_ID, "FOO", "사유");
    }

    // ---- AC-7: 회수 후 재부여 ----

    @Test
    void grantManualRegrantsAfterRevoke() {
        // 회수된 행은 ux_user_badges_active 부분유니크(WHERE revoked_at IS NULL)에서 제외되므로
        // existsActive=false → 동일 키 재부여가 가능하다.
        when(usersMapper.getUser(USER_ID)).thenReturn(new UserData());
        when(userBadgesMapper.existsActive(USER_ID, "REVIEWER")).thenReturn(false);

        BadgeService.GrantResult result = service().grantManual(USER_ID, "REVIEWER", AWARDED_BY);

        assertThat(result).isEqualTo(BadgeService.GrantResult.GRANTED);
        verify(userBadgesMapper).insert(USER_ID, "REVIEWER", AWARDED_BY);
    }
}
