package com.eactive.eai.custom.transformer.function;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Stack;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.nfunk.jep.ParseException;
import org.nfunk.jep.function.PostfixMathCommand;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;

import com.eactive.eai.common.security.CryptoModuleService;
import com.eactive.eai.common.util.ApplicationContextProvider;

@TestMethodOrder(MethodOrderer.DisplayName.class)
class DJBCryptoFunctionTest {

    private static final String MODULE_NAME  = "TEST_MODULE";
    private static final String PLAIN_TEXT   = "plaintext-value";
    private static final byte[] PLAIN_BYTES  = PLAIN_TEXT.getBytes(StandardCharsets.UTF_8);
    private static final byte[] CIPHER_BYTES = new byte[]{0x10, 0x20, 0x30, 0x40};
    private static final String ENC_BASE64   = Base64.getEncoder().encodeToString(CIPHER_BYTES);

    private static GenericApplicationContext ctx;
    private static CryptoModuleService       mockService;

    private DJBDecrypt decryt;
    private DJBEncrypt encryt;

    // =========================================================================
    // 클래스 초기화
    // =========================================================================

    @BeforeAll
    static void setUpClass() throws Exception {
        mockService = mock(CryptoModuleService.class);

        ctx = new GenericApplicationContext();
        ctx.getBeanFactory().registerSingleton("cryptoModuleService", mockService);
        ctx.registerBeanDefinition("applicationContextProvider",
                BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
                        .getBeanDefinition());
        ctx.refresh();
    }

    @AfterAll
    static void tearDownClass() {
        if (ctx != null) ctx.close();
    }

    @BeforeEach
    void setUp() throws Exception {
        reset(mockService);
        when(mockService.decrypt(any(), anyMap(), isNull(), any(byte[].class))).thenReturn(PLAIN_BYTES);
        when(mockService.encrypt(any(), anyMap(), isNull(), any(byte[].class))).thenReturn(CIPHER_BYTES);
        decryt = new DJBDecrypt();
        encryt = new DJBEncrypt();
    }

    // =========================================================================
    // 1. DJBErpDecryt
    // =========================================================================

    @Test
    @DisplayName("1-1. 정상 복호화: (encBase64, moduleName) → 평문이 스택에 push된다")
    void decryt_normalCase_pushesPlainText() throws Exception {
        Stack<Object> stack = buildStack(ENC_BASE64, MODULE_NAME);
        setParamCount(decryt, 2);

        decryt.run(stack);

        assertEquals(PLAIN_TEXT, stack.pop());
        assertTrue(stack.isEmpty(), "결과 push 후 스택에 잔여 항목이 없어야 한다");
        verify(mockService).decrypt(eq(MODULE_NAME), anyMap(), isNull(), any(byte[].class));
    }

    @Test
    @DisplayName("1-2. 파라미터 수가 2가 아니면 ParseException")
    void decryt_wrongParamCount_throwsParseException() throws Exception {
        Stack<Object> stack = buildStack(ENC_BASE64, MODULE_NAME);
        setParamCount(decryt, 3);

        assertThrows(ParseException.class, () -> decryt.run(stack));
    }

    @Test
    @DisplayName("1-3. 유효하지 않은 Base64 입력 → ParseException")
    void decryt_invalidBase64_throwsParseException() throws Exception {
        Stack<Object> stack = buildStack("not-valid-base64!!!", MODULE_NAME);
        setParamCount(decryt, 2);

        assertThrows(ParseException.class, () -> decryt.run(stack));
    }

    @Test
    @DisplayName("1-4. 암호화 서비스 예외 → 'DJBErpDecryt 복호화 실패' ParseException으로 래핑")
    void decryt_serviceThrows_wrapsInParseException() throws Exception {
        when(mockService.decrypt(any(), anyMap(), isNull(), any(byte[].class)))
                .thenThrow(new RuntimeException("모듈 오류"));
        Stack<Object> stack = buildStack(ENC_BASE64, MODULE_NAME);
        setParamCount(decryt, 2);

        ParseException ex = assertThrows(ParseException.class, () -> decryt.run(stack));
        assertTrue(ex.getMessage().contains("DJBErpDecryt 복호화 실패"));
    }

    @Test
    @DisplayName("1-5. moduleName이 CryptoModuleService에 그대로 전달된다")
    void decryt_moduleNamePassedToService() throws Exception {
        String customModule = "CUSTOM_AES_MODULE";
        Stack<Object> stack = buildStack(ENC_BASE64, customModule);
        setParamCount(decryt, 2);

        decryt.run(stack);

        verify(mockService).decrypt(eq(customModule), anyMap(), isNull(), any(byte[].class));
    }

    // =========================================================================
    // 2. DJBErpEncryt
    // =========================================================================

    @Test
    @DisplayName("2-1. 정상 암호화: (plainText, moduleName) → Base64가 스택에 push된다")
    void encryt_normalCase_pushesBase64() throws Exception {
        Stack<Object> stack = buildStack(PLAIN_TEXT, MODULE_NAME);
        setParamCount(encryt, 2);

        encryt.run(stack);

        assertEquals(ENC_BASE64, stack.pop());
        assertTrue(stack.isEmpty(), "결과 push 후 스택에 잔여 항목이 없어야 한다");
        verify(mockService).encrypt(eq(MODULE_NAME), anyMap(), isNull(), any(byte[].class));
    }

    @Test
    @DisplayName("2-2. 파라미터 수가 2가 아니면 ParseException")
    void encryt_wrongParamCount_throwsParseException() throws Exception {
        Stack<Object> stack = buildStack(PLAIN_TEXT, MODULE_NAME);
        setParamCount(encryt, 3);

        assertThrows(ParseException.class, () -> encryt.run(stack));
    }

    @Test
    @DisplayName("2-3. 암호화 서비스 예외 → 'DJBErpEncryt 암호화 실패' ParseException으로 래핑")
    void encryt_serviceThrows_wrapsInParseException() throws Exception {
        when(mockService.encrypt(any(), anyMap(), isNull(), any(byte[].class)))
                .thenThrow(new RuntimeException("모듈 오류"));
        Stack<Object> stack = buildStack(PLAIN_TEXT, MODULE_NAME);
        setParamCount(encryt, 2);

        ParseException ex = assertThrows(ParseException.class, () -> encryt.run(stack));
        assertTrue(ex.getMessage().contains("DJBErpEncryt 암호화 실패"));
    }

    @Test
    @DisplayName("2-4. moduleName이 CryptoModuleService에 그대로 전달된다")
    void encryt_moduleNamePassedToService() throws Exception {
        String customModule = "CUSTOM_AES_MODULE";
        Stack<Object> stack = buildStack(PLAIN_TEXT, customModule);
        setParamCount(encryt, 2);

        encryt.run(stack);

        verify(mockService).encrypt(eq(customModule), anyMap(), isNull(), any(byte[].class));
    }

    @Test
    @DisplayName("2-5. plainText가 UTF-8 바이트로 변환되어 서비스에 전달된다")
    void encryt_plainTextEncodedAsUtf8() throws Exception {
        String korean = "한글평문";
        Stack<Object> stack = buildStack(korean, MODULE_NAME);
        setParamCount(encryt, 2);

        encryt.run(stack);

        verify(mockService).encrypt(eq(MODULE_NAME), anyMap(), isNull(),
                eq(korean.getBytes(StandardCharsets.UTF_8)));
    }

    // =========================================================================
    // 헬퍼
    // =========================================================================

    @SuppressWarnings({"rawtypes", "unchecked"})
    private Stack<Object> buildStack(Object... params) {
        Stack stack = new Stack();
        for (Object p : params) {
            stack.push(p);
        }
        return stack;
    }

    private void setParamCount(PostfixMathCommand cmd, int count) throws Exception {
        Field field = PostfixMathCommand.class.getDeclaredField("curNumberOfParameters");
        field.setAccessible(true);
        field.set(cmd, count);
    }
}
