package com.zioinfo.kintex.security;

import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.system.loginhistory.LoginHistoryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * 로그인 실패 잠금 카운터/정책 단위테스트 — 임계 초과 시 잠금, 성공 시 리셋, 계정 미존재 시 무변경.
 */
class LoginAttemptServiceTest {

    private AccountSecurityMapper mapper;
    private LoginAttemptService service;

    @BeforeEach
    void setUp() {
        mapper = mock(AccountSecurityMapper.class);
        SecurityPolicyProperties props = new SecurityPolicyProperties();
        props.getLockout().setMaxFailedAttempts(5);
        props.getLockout().setLockMinutes(30);
        service = new LoginAttemptService(mapper, props, mock(LoginHistoryService.class));
    }

    private Map<String, Object> row(int failedCount, OffsetDateTime lockedUntil) {
        Map<String, Object> m = new HashMap<>();
        m.put("failedCount", failedCount);
        m.put("lockedUntil", lockedUntil);
        return m;
    }

    @Test
    void locksAccountWhenFailuresReachThreshold() {
        // 직전 4회 실패 → 이번 실패가 5회째 → 잠금.
        service.recordFailure("u@kintex.test", row(4, null));
        verify(mapper).incrementFailure("u@kintex.test");
        ArgumentCaptor<OffsetDateTime> until = ArgumentCaptor.forClass(OffsetDateTime.class);
        verify(mapper).lockUntil(org.mockito.ArgumentMatchers.eq("u@kintex.test"), until.capture());
        // 잠금 만료는 미래(대략 30분 후)여야 한다.
        org.junit.jupiter.api.Assertions.assertTrue(until.getValue().isAfter(OffsetDateTime.now()));
    }

    @Test
    void doesNotLockBeforeThreshold() {
        service.recordFailure("u@kintex.test", row(1, null)); // 2회째
        verify(mapper).incrementFailure("u@kintex.test");
        verify(mapper, never()).lockUntil(org.mockito.ArgumentMatchers.anyString(),
                org.mockito.ArgumentMatchers.any());
    }

    @Test
    void missingAccountDoesNotMutateState() {
        service.recordFailure("nobody@kintex.test", null);
        verify(mapper, never()).incrementFailure(org.mockito.ArgumentMatchers.anyString());
        verify(mapper, never()).lockUntil(org.mockito.ArgumentMatchers.anyString(),
                org.mockito.ArgumentMatchers.any());
    }

    @Test
    void assertNotLockedThrowsWhenLocked() {
        Map<String, Object> locked = row(5, OffsetDateTime.now().plusMinutes(10));
        ApiException ex = assertThrows(ApiException.class, () -> service.assertNotLocked(locked));
        assertEquals(ErrorCode.ACCOUNT_LOCKED, ex.getCode());
    }

    @Test
    void assertNotLockedPassesWhenLockExpired() {
        Map<String, Object> expired = row(5, OffsetDateTime.now().minusMinutes(1));
        assertDoesNotThrow(() -> service.assertNotLocked(expired));
    }

    @Test
    void recordSuccessClearsLoginState() {
        service.recordSuccess("user-1");
        verify(mapper, times(1)).clearLoginState("user-1");
    }
}
