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

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

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.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;
import com.eactive.eai.transformer.engine.Parser;

/**
 * DJBEncrypt / DJBDecrypt — Parser 표현식 통합 테스트.
 *
 * DJBCryptoFunctionTest 는 Stack 직접 호출 방식으로 run() 을 검증하고,
 * 이 클래스는 Parser 를 통해 JEP 수식으로 함수를 호출하는 통합 경로를 검증한다.
 */
@TestMethodOrder(MethodOrderer.DisplayName.class)
@DisplayName("DJBEncrypt / DJBDecrypt — Parser 변수/상수 혼합 테스트")
class DJBCryptoFunctionParserTest {

    private static final String MODULE       = "AES_MODULE";
    private static final String PLAIN_TEXT   = "hello-djb";
    private static final byte[] PLAIN_BYTES  = PLAIN_TEXT.getBytes(StandardCharsets.UTF_8);
    private static final byte[] CIPHER_BYTES = {0x01, 0x02, 0x03, 0x04};
    private static final String ENC_BASE64   = Base64.getEncoder().encodeToString(CIPHER_BYTES);

    private static GenericApplicationContext ctx;
    private static CryptoModuleService       mockService;

    private Parser parser;

    // =========================================================================
    // 클래스 초기화 — mock CryptoModuleService 를 Spring 컨텍스트에 등록
    // =========================================================================

    @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.encrypt(any(), anyMap(), isNull(), any(byte[].class))).thenReturn(CIPHER_BYTES);
        when(mockService.decrypt(any(), anyMap(), isNull(), any(byte[].class))).thenReturn(PLAIN_BYTES);

        parser = new Parser();
        parser.getJep().addFunction("DJBEncrypt", new DJBEncrypt());
        parser.getJep().addFunction("DJBDecrypt", new DJBDecrypt());
    }

    // =========================================================================
    // 1. DJBEncrypt
    // =========================================================================

    @Test
    @DisplayName("1-1. 암호화 — 평문 변수 + 상수 모듈명 → Base64 반환")
    void encrypt_variablePlain_constantModule() throws Exception {
        parser.parse("DJBEncrypt(plain, \"" + MODULE + "\")");
        parser.addVariable("plain", PLAIN_TEXT);

        assertEquals(ENC_BASE64, parser.getValueAsObject());
    }

    @Test
    @DisplayName("1-2. 암호화 — 평문·모듈명 모두 변수")
    void encrypt_bothVariables() throws Exception {
        parser.parse("DJBEncrypt(plain, moduleName)");
        parser.addVariable("plain", PLAIN_TEXT);
        parser.addVariable("moduleName", MODULE);

        assertEquals(ENC_BASE64, parser.getValueAsObject());
    }

    @Test
    @DisplayName("1-3. 암호화 — parse 1회, 변수값 교체 반복 평가")
    void encrypt_reuseParser_multipleEvaluations() throws Exception {
        // identity: plainBytes 그대로 반환 → Base64 encoding 경로 검증에 집중
        when(mockService.encrypt(any(), anyMap(), isNull(), any(byte[].class)))
                .thenAnswer(inv -> (byte[]) inv.getArgument(3));

        parser.parse("DJBEncrypt(plain, \"" + MODULE + "\")");

        parser.addVariable("plain", "first");
        String r1 = (String) parser.getValueAsObject();

        parser.addVariable("plain", "second");
        String r2 = (String) parser.getValueAsObject();

        assertEquals(Base64.getEncoder().encodeToString("first".getBytes(StandardCharsets.UTF_8)), r1);
        assertEquals(Base64.getEncoder().encodeToString("second".getBytes(StandardCharsets.UTF_8)), r2);
    }

    @Test
    @DisplayName("1-4. 암호화 — 한글 평문이 UTF-8 인코딩되어 서비스에 전달된다")
    void encrypt_koreanPlainText_utf8Encoded() throws Exception {
        String korean = "한글평문";
        parser.parse("DJBEncrypt(plain, \"" + MODULE + "\")");
        parser.addVariable("plain", korean);
        parser.getValueAsObject();

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

    // =========================================================================
    // 2. DJBDecrypt
    // =========================================================================

    @Test
    @DisplayName("2-1. 복호화 — Base64 변수 + 상수 모듈명 → 평문 반환")
    void decrypt_variableEnc_constantModule() throws Exception {
        parser.parse("DJBDecrypt(enc, \"" + MODULE + "\")");
        parser.addVariable("enc", ENC_BASE64);

        assertEquals(PLAIN_TEXT, parser.getValueAsObject());
    }

    @Test
    @DisplayName("2-2. 복호화 — Base64·모듈명 모두 변수")
    void decrypt_bothVariables() throws Exception {
        parser.parse("DJBDecrypt(enc, moduleName)");
        parser.addVariable("enc", ENC_BASE64);
        parser.addVariable("moduleName", MODULE);

        assertEquals(PLAIN_TEXT, parser.getValueAsObject());
    }

    @Test
    @DisplayName("2-3. 복호화 — parse 1회, 변수값 교체 반복 평가")
    void decrypt_reuseParser_multipleEvaluations() throws Exception {
        // identity: cipherBytes 그대로 반환 → Base64 decoding 경로 검증에 집중
        when(mockService.decrypt(any(), anyMap(), isNull(), any(byte[].class)))
                .thenAnswer(inv -> (byte[]) inv.getArgument(3));

        parser.parse("DJBDecrypt(enc, \"" + MODULE + "\")");

        String firstEnc  = Base64.getEncoder().encodeToString("first".getBytes(StandardCharsets.UTF_8));
        String secondEnc = Base64.getEncoder().encodeToString("second".getBytes(StandardCharsets.UTF_8));

        parser.addVariable("enc", firstEnc);
        assertEquals("first", parser.getValueAsObject());

        parser.addVariable("enc", secondEnc);
        assertEquals("second", parser.getValueAsObject());
    }

    // =========================================================================
    // 3. 라운드트립
    // =========================================================================

    @Test
    @DisplayName("3-1. 라운드트립 — 암호화 결과를 복호화하면 원문과 일치")
    void roundTrip_encryptThenDecrypt_returnsOriginal() throws Exception {
        // 암호화
        parser.parse("DJBEncrypt(plain, \"" + MODULE + "\")");
        parser.addVariable("plain", PLAIN_TEXT);
        String encryptedBase64 = (String) parser.getValueAsObject();

        // 복호화
        parser.parse("DJBDecrypt(enc, \"" + MODULE + "\")");
        parser.addVariable("enc", encryptedBase64);

        assertEquals(PLAIN_TEXT, parser.getValueAsObject());
    }

    @Test
    @DisplayName("3-2. 라운드트립 — 한글 평문")
    void roundTrip_koreanPlainText() throws Exception {
        String korean = "한글평문테스트";
        when(mockService.encrypt(any(), anyMap(), isNull(), any(byte[].class)))
                .thenReturn(korean.getBytes(StandardCharsets.UTF_8));
        when(mockService.decrypt(any(), anyMap(), isNull(), any(byte[].class)))
                .thenReturn(korean.getBytes(StandardCharsets.UTF_8));

        parser.parse("DJBEncrypt(plain, \"" + MODULE + "\")");
        parser.addVariable("plain", korean);
        String enc = (String) parser.getValueAsObject();

        parser.parse("DJBDecrypt(enc, \"" + MODULE + "\")");
        parser.addVariable("enc", enc);
        assertEquals(korean, parser.getValueAsObject());
    }

    // =========================================================================
    // 4. 오류 처리
    // =========================================================================

    @Test
    @DisplayName("4-1. 암호화 — 파라미터 1개이면 예외")
    void encrypt_singleParam_throwsException() {
        assertThrows(Exception.class, () -> {
            parser.parse("DJBEncrypt(plain)");
            parser.addVariable("plain", PLAIN_TEXT);
            parser.getValueAsObject();
        });
    }

    @Test
    @DisplayName("4-2. 복호화 — 파라미터 1개이면 예외")
    void decrypt_singleParam_throwsException() {
        assertThrows(Exception.class, () -> {
            parser.parse("DJBDecrypt(enc)");
            parser.addVariable("enc", ENC_BASE64);
            parser.getValueAsObject();
        });
    }

    @Test
    @DisplayName("4-3. 복호화 — 유효하지 않은 Base64 상수이면 예외")
    void decrypt_invalidBase64Constant_throwsException() {
        assertThrows(Exception.class, () -> {
            parser.parse("DJBDecrypt(\"!!invalid!!\", \"" + MODULE + "\")");
            parser.getValueAsObject();
        });
    }
}
