package com.ga.common.code;

import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * CommonCodeService 단위 테스트.
 *
 * 주의: @Cacheable / @CacheEvict 는 Spring 프록시 단계에서 동작하므로
 * 단위 테스트(직접 인스턴스 호출)에서는 캐시가 끼지 않는다 — 비즈니스 로직만 검증.
 */
@ExtendWith(MockitoExtension.class)
class CommonCodeServiceTest {

    @Mock CommonCodeMapper mapper;
    @InjectMocks CommonCodeService service;

    private CommonCodeVO code(String groupCode, String code, String name) {
        CommonCodeVO c = new CommonCodeVO();
        c.setGroupCode(groupCode);
        c.setCode(code);
        c.setCodeName(name);
        return c;
    }

    private CommonCodeGroupVO group(String groupCode, String isSystem) {
        CommonCodeGroupVO g = new CommonCodeGroupVO();
        g.setGroupCode(groupCode);
        g.setIsSystem(isSystem);
        return g;
    }

    /** 필요한 테스트에서만 호출 (strict stubbing) */
    private void stubGenderGroup() {
        when(mapper.selectActiveCodes("GENDER")).thenReturn(List.of(
                code("GENDER", "M", "남자"),
                code("GENDER", "F", "여자")
        ));
    }

    @Test
    @DisplayName("getCodeName: 매칭 성공")
    void getCodeName_hit() {
        stubGenderGroup();
        assertThat(service.getCodeName("GENDER", "M")).isEqualTo("남자");
        assertThat(service.getCodeName("GENDER", "F")).isEqualTo("여자");
    }

    @Test
    @DisplayName("getCodeName: 미존재 코드 → null")
    void getCodeName_miss() {
        stubGenderGroup();
        assertThat(service.getCodeName("GENDER", "X")).isNull();
    }

    @Test
    @DisplayName("getCodeName: code null → null (DB 조회 안함)")
    void getCodeName_nullCode() {
        assertThat(service.getCodeName("GENDER", null)).isNull();
        verify(mapper, never()).selectActiveCodes(any());
    }

    @Test
    @DisplayName("validateCode: 미존재 시 INVALID_PARAMETER 예외")
    void validateCode_invalid() {
        stubGenderGroup();
        assertThatThrownBy(() -> service.validateCode("GENDER", "X"))
                .isInstanceOf(BizException.class)
                .extracting(e -> ((BizException) e).getErrorCode())
                .isEqualTo(ErrorCode.INVALID_PARAMETER);
    }

    @Test
    @DisplayName("validateCode: 존재하면 예외 없음")
    void validateCode_ok() {
        stubGenderGroup();
        service.validateCode("GENDER", "M"); // no throw
    }

    @Test
    @DisplayName("getGroup: 미존재 시 NOT_FOUND")
    void getGroup_notFound() {
        when(mapper.selectGroup("MISSING")).thenReturn(null);

        assertThatThrownBy(() -> service.getGroup("MISSING"))
                .isInstanceOf(BizException.class)
                .extracting(e -> ((BizException) e).getErrorCode())
                .isEqualTo(ErrorCode.NOT_FOUND);
    }

    @Test
    @DisplayName("createGroup: 중복 그룹 코드 → DUPLICATE_DATA")
    void createGroup_duplicate() {
        when(mapper.selectGroup("DUP")).thenReturn(group("DUP", "N"));
        CommonCodeGroupVO vo = group("DUP", "N");

        assertThatThrownBy(() -> service.createGroup(vo))
                .isInstanceOf(BizException.class)
                .extracting(e -> ((BizException) e).getErrorCode())
                .isEqualTo(ErrorCode.DUPLICATE_DATA);
        verify(mapper, never()).insertGroup(any());
    }

    @Test
    @DisplayName("createGroup: 신규 → insertGroup 호출")
    void createGroup_ok() {
        when(mapper.selectGroup("NEW")).thenReturn(null);
        CommonCodeGroupVO vo = group("NEW", "N");

        service.createGroup(vo);

        verify(mapper).insertGroup(vo);
    }

    @Test
    @DisplayName("updateGroup: 시스템 그룹은 SYSTEM_CODE_LOCKED")
    void updateGroup_systemLocked() {
        when(mapper.selectGroup("SYS")).thenReturn(group("SYS", "Y"));
        CommonCodeGroupVO vo = group("SYS", "Y");

        assertThatThrownBy(() -> service.updateGroup(vo))
                .isInstanceOf(BizException.class)
                .extracting(e -> ((BizException) e).getErrorCode())
                .isEqualTo(ErrorCode.SYSTEM_CODE_LOCKED);
        verify(mapper, never()).updateGroup(any());
    }

    @Test
    @DisplayName("deleteGroup: 일반 그룹은 삭제 가능")
    void deleteGroup_ok() {
        when(mapper.selectGroup("USER_GRP")).thenReturn(group("USER_GRP", "N"));

        service.deleteGroup("USER_GRP");

        verify(mapper).deleteGroup("USER_GRP");
    }

    @Test
    @DisplayName("deleteGroup: 시스템 그룹은 SYSTEM_CODE_LOCKED")
    void deleteGroup_systemLocked() {
        when(mapper.selectGroup("SYS")).thenReturn(group("SYS", "Y"));

        assertThatThrownBy(() -> service.deleteGroup("SYS"))
                .isInstanceOf(BizException.class)
                .extracting(e -> ((BizException) e).getErrorCode())
                .isEqualTo(ErrorCode.SYSTEM_CODE_LOCKED);
        verify(mapper, never()).deleteGroup(any());
    }

    @Test
    @DisplayName("createCode: 동일 (group, code) 중복 → DUPLICATE_DATA")
    void createCode_duplicate() {
        when(mapper.existsByGroupAndCode("GENDER", "M")).thenReturn(1);

        assertThatThrownBy(() -> service.createCode(code("GENDER", "M", "남자")))
                .isInstanceOf(BizException.class);
        verify(mapper, never()).insertCode(any());
    }

    @Test
    @DisplayName("updateCode: 시스템 그룹의 code/groupCode 는 강제로 원본 유지")
    void updateCode_systemForcesIdentity() {
        CommonCodeVO old = code("GENDER", "M", "남자");
        old.setCodeId(1L);
        when(mapper.selectCode(1L)).thenReturn(old);
        when(mapper.selectGroup("GENDER")).thenReturn(group("GENDER", "Y")); // 시스템 그룹

        // 사용자가 임의로 code/groupCode 바꾸려 시도
        CommonCodeVO update = code("OTHER", "X", "변경시도");
        update.setCodeId(1L);

        service.updateCode(update);

        // 시스템 그룹이므로 code/groupCode 가 원본으로 강제 복원됨
        assertThat(update.getCode()).isEqualTo("M");
        assertThat(update.getGroupCode()).isEqualTo("GENDER");
        verify(mapper).updateCode(update);
    }

    @Test
    @DisplayName("deleteCode: 시스템 그룹은 SYSTEM_CODE_LOCKED")
    void deleteCode_systemLocked() {
        CommonCodeVO c = code("GENDER", "M", "남자");
        c.setCodeId(1L);
        when(mapper.selectCode(1L)).thenReturn(c);
        when(mapper.selectGroup("GENDER")).thenReturn(group("GENDER", "Y"));

        assertThatThrownBy(() -> service.deleteCode(1L))
                .isInstanceOf(BizException.class)
                .extracting(e -> ((BizException) e).getErrorCode())
                .isEqualTo(ErrorCode.SYSTEM_CODE_LOCKED);
        verify(mapper, never()).deleteCode(any());
    }
}
