package com.ruoyi.loanpricing.service;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;

class LoanPricingModelServiceTest
{
    @Test
    void shouldPassPlainCustNameAndIdNumBeforeInvokePersonalModel() throws Exception
    {
        TestContext context = createContext(personalWorkflow(1L));

        context.service.invokeModelAsync(1L);

        assertEquals("张三", context.modelService.personalRequest.getCustName());
        assertEquals("110101199001011234", context.modelService.personalRequest.getIdNum());
        assertEquals("2.15", context.modelService.personalRequest.getCouponRate());
        assertEquals(1, context.modelService.personalCalls.get());
        assertEquals(0, context.modelService.corporateCalls.get());
        assertEquals(1, context.retailInsertCount.get());
        assertEquals(0, context.corpInsertCount.get());
    }

    @Test
    void shouldOnlyWriteModelOutputIdBackWhenUpdatingWorkflow() throws Exception
    {
        TestContext context = createContext(personalWorkflow(2L));

        context.service.invokeModelAsync(2L);

        LoanPricingWorkflow updatedWorkflow = context.updatedWorkflow.get();
        assertEquals(2L, updatedWorkflow.getId());
        assertEquals(null, updatedWorkflow.getCustName());
        assertEquals(null, updatedWorkflow.getIdNum());
    }

    @Test
    void shouldNormalizeCorporateUploadParamsBeforeInvokeCorporateModel() throws Exception
    {
        LoanPricingWorkflow workflow = new LoanPricingWorkflow();
        workflow.setId(3L);
        workflow.setCustType("企业");
        workflow.setCustName("测试科技有限公司");
        workflow.setIdNum("91110000100000000X");
        workflow.setRepayMethod("分期");
        workflow.setResCover("true");
        workflow.setIsGreenLoan("true");
        workflow.setIsTradeBuildEnt("false");
        workflow.setCollThirdParty("true");
        workflow.setCouponRate("2.35");
        TestContext context = createContext(workflow);

        context.service.invokeModelAsync(3L);

        ModelInvokeDTO request = context.modelService.corporateRequest;
        assertEquals("分期", request.getRepayMethod());
        assertEquals("1", request.getResCover());
        assertEquals("1", request.getIsGreenLoan());
        assertEquals("0", request.getIsTradeBuildEnt());
        assertEquals("1", request.getCollThirdParty());
        assertEquals("2.35", request.getCouponRate());
        assertEquals(0, context.modelService.personalCalls.get());
        assertEquals(1, context.modelService.corporateCalls.get());
        assertEquals(0, context.retailInsertCount.get());
        assertEquals(1, context.corpInsertCount.get());
    }

    @Test
    void shouldOverwriteExistingCorporateModelOutputWhenReinvokingModel() throws Exception
    {
        LoanPricingWorkflow workflow = new LoanPricingWorkflow();
        workflow.setId(4L);
        workflow.setModelOutputId(88L);
        workflow.setCustType("企业");
        workflow.setCustName("测试科技有限公司");
        workflow.setIdNum("91110000100000000X");
        TestContext context = createContext(workflow);

        context.service.reinvokeModelAndOverwrite(4L);

        assertEquals(1, context.modelService.corporateCalls.get());
        assertEquals(0, context.corpInsertCount.get());
        assertEquals(1, context.corpUpdateCount.get());
    }

    private static LoanPricingWorkflow personalWorkflow(Long id)
    {
        LoanPricingWorkflow workflow = new LoanPricingWorkflow();
        workflow.setId(id);
        workflow.setCustType("个人");
        workflow.setCustName("张三");
        workflow.setIdNum("110101199001011234");
        workflow.setCouponRate("2.15");
        return workflow;
    }

    private static TestContext createContext(LoanPricingWorkflow workflow) throws Exception
    {
        TestContext context = new TestContext();
        context.service = new LoanPricingModelService();
        context.modelService = new CapturingModelService();
        context.workflow = workflow;
        context.updatedWorkflow = new AtomicReference<>();
        context.retailInsertCount = new AtomicInteger();
        context.retailUpdateCount = new AtomicInteger();
        context.corpInsertCount = new AtomicInteger();
        context.corpUpdateCount = new AtomicInteger();

        setField(context.service, "modelService", context.modelService);
        setField(context.service, "loanPricingWorkflowMapper",
                workflowMapper(context.workflow, context.updatedWorkflow));
        setField(context.service, "modelRetailOutputFieldsMapper",
                savingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount, context.retailUpdateCount));
        setField(context.service, "modelCorpOutputFieldsMapper",
                savingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount, context.corpUpdateCount));
        return context;
    }

    private static LoanPricingWorkflowMapper workflowMapper(
            LoanPricingWorkflow workflow, AtomicReference<LoanPricingWorkflow> updatedWorkflow)
    {
        return proxy(LoanPricingWorkflowMapper.class, (proxy, method, args) -> {
            if ("selectById".equals(method.getName()))
            {
                return workflow;
            }
            if ("updateById".equals(method.getName()))
            {
                updatedWorkflow.set((LoanPricingWorkflow) args[0]);
                return 1;
            }
            return defaultValue(method);
        });
    }

    private static <T> T savingMapper(Class<T> mapperClass, AtomicInteger insertCount, AtomicInteger updateCount)
    {
        return proxy(mapperClass, (proxy, method, args) -> {
            if ("insert".equals(method.getName()))
            {
                insertCount.incrementAndGet();
                return 1;
            }
            if ("updateById".equals(method.getName()))
            {
                updateCount.incrementAndGet();
                return 1;
            }
            return defaultValue(method);
        });
    }

    @SuppressWarnings("unchecked")
    private static <T> T proxy(Class<T> mapperClass, InvocationHandler invocationHandler)
    {
        return (T) Proxy.newProxyInstance(
                mapperClass.getClassLoader(), new Class<?>[] { mapperClass }, invocationHandler);
    }

    private static Object defaultValue(Method method)
    {
        if (method.getReturnType().equals(boolean.class))
        {
            return false;
        }
        if (method.getReturnType().isPrimitive())
        {
            return 0;
        }
        return null;
    }

    private static void setField(Object target, String fieldName, Object value) throws Exception
    {
        Field field = LoanPricingModelService.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(target, value);
    }

    private static class TestContext
    {
        private LoanPricingModelService service;
        private CapturingModelService modelService;
        private LoanPricingWorkflow workflow;
        private AtomicReference<LoanPricingWorkflow> updatedWorkflow;
        private AtomicInteger retailInsertCount;
        private AtomicInteger retailUpdateCount;
        private AtomicInteger corpInsertCount;
        private AtomicInteger corpUpdateCount;
    }

    private static class CapturingModelService extends ModelService
    {
        private final AtomicInteger personalCalls = new AtomicInteger();
        private final AtomicInteger corporateCalls = new AtomicInteger();
        private ModelInvokeDTO personalRequest;
        private ModelInvokeDTO corporateRequest;

        @Override
        public ModelRetailOutputFields invokePersonalModel(ModelInvokeDTO modelInvokeDTO)
        {
            personalCalls.incrementAndGet();
            personalRequest = modelInvokeDTO;
            return new ModelRetailOutputFields();
        }

        @Override
        public ModelCorpOutputFields invokeCorporateModel(ModelInvokeDTO modelInvokeDTO)
        {
            corporateCalls.incrementAndGet();
            corporateRequest = modelInvokeDTO;
            return new ModelCorpOutputFields();
        }
    }

}
