package com.ruoyi.web.modules.pay.support;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.web.modules.pay.config.WeChatMiniAppProperties;
import com.ruoyi.web.modules.pay.config.WeChatPayProperties;
import com.ruoyi.web.modules.pay.constant.PayConstants;

@ExtendWith(MockitoExtension.class)
class WeChatPaySupportTest
{
    @Mock
    private WeChatMiniAppProperties miniAppProperties;

    @Mock
    private org.springframework.web.client.RestTemplate restTemplate;

    @InjectMocks
    private WeChatPaySupport paySupport;

    @TempDir
    Path tempDir;

    private PrivateKey privateKey;

    private PublicKey publicKey;

    private WeChatPayProperties payProperties;

    @BeforeEach
    void setUp() throws Exception
    {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048);
        KeyPair keyPair = generator.generateKeyPair();
        privateKey = keyPair.getPrivate();
        publicKey = keyPair.getPublic();

        Path privatePath = tempDir.resolve("private.pem");
        Path publicPath = tempDir.resolve("public.pem");
        Files.write(privatePath, toPkcs8Pem(privateKey).getBytes(StandardCharsets.UTF_8));
        Files.write(publicPath, toPublicPem(publicKey).getBytes(StandardCharsets.UTF_8));

        payProperties = new WeChatPayProperties();
        payProperties.setEnabled(true);
        payProperties.setMock(false);
        payProperties.setPrivateKeyPath(privatePath.toString());
        payProperties.setPlatformPublicKeyPath(publicPath.toString());
        payProperties.setNotifyVerifyEnabled(true);
        payProperties.setNotifyTimestampToleranceSeconds(300);
        ReflectionTestUtils.setField(paySupport, "payProperties", payProperties);
    }

    @Test
    void verifyNotifySignature_validSignature_passes()
    {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonce = "nonce123";
        String rawBody = "{\"id\":\"evt\"}";
        String signature = signNotify(timestamp, nonce, rawBody);

        assertDoesNotThrow(() -> paySupport.verifyNotifySignature(timestamp, nonce, rawBody, signature));
    }

    @Test
    void verifyNotifySignature_invalidSignature_rejected()
    {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonce = "nonce123";
        String rawBody = "{\"id\":\"evt\"}";

        assertThrows(ServiceException.class,
                () -> paySupport.verifyNotifySignature(timestamp, nonce, rawBody, "invalid"));
    }

    @Test
    void verifyNotifySignature_expiredTimestamp_rejected()
    {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000 - 1000);
        String nonce = "nonce123";
        String rawBody = "{\"id\":\"evt\"}";
        String signature = signNotify(timestamp, nonce, rawBody);

        assertThrows(ServiceException.class,
                () -> paySupport.verifyNotifySignature(timestamp, nonce, rawBody, signature));
    }

    @Test
    void verifyNotifySignature_skippedWhenMockMode()
    {
        payProperties.setEnabled(false);
        payProperties.setMock(true);

        assertDoesNotThrow(() -> paySupport.verifyNotifySignature("", "", "", ""));
    }

    @Test
    void verifyNotifySignature_missingPublicKey_rejected()
    {
        payProperties.setPlatformPublicKeyPath("");
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonce = "nonce123";
        String rawBody = "{\"id\":\"evt\"}";
        String signature = signNotify(timestamp, nonce, rawBody);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> paySupport.verifyNotifySignature(timestamp, nonce, rawBody, signature));
        assert ex.getMessage().equals(PayConstants.MSG_PAY_NOTIFY_VERIFY_KEY_MISSING);
    }

    private String signNotify(String timestamp, String nonce, String rawBody)
    {
        try
        {
            String message = timestamp + "\n" + nonce + "\n" + rawBody + "\n";
            Signature signer = Signature.getInstance("SHA256withRSA");
            signer.initSign(privateKey);
            signer.update(message.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(signer.sign());
        }
        catch (Exception ex)
        {
            throw new RuntimeException(ex);
        }
    }

    private static String toPkcs8Pem(PrivateKey key)
    {
        String base64 = Base64.getEncoder().encodeToString(key.getEncoded());
        return "-----BEGIN PRIVATE KEY-----\n" + base64 + "\n-----END PRIVATE KEY-----\n";
    }

    private static String toPublicPem(PublicKey key)
    {
        String base64 = Base64.getEncoder().encodeToString(key.getEncoded());
        return "-----BEGIN PUBLIC KEY-----\n" + base64 + "\n-----END PUBLIC KEY-----\n";
    }
}
