package com.red.circle.other.app.command.user;

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.red.circle.external.inner.endpoint.message.ImMessageClient;
import com.red.circle.external.inner.model.cmd.message.CustomC2cMsgBodyCmd;
import com.red.circle.framework.core.dto.ReqSysOrigin;
import com.red.circle.framework.dto.ResultResponse;
import com.red.circle.other.app.common.cp.CpRelationBroadcastMqClient;
import com.red.circle.other.app.dto.cmd.user.relation.cp.ProcessApplyCmd;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService;
import com.red.circle.other.infra.database.rds.entity.user.user.CpApply;
import com.red.circle.other.infra.database.rds.service.user.user.CpApplyService;
import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService;
import com.red.circle.other.infra.database.rds.service.user.user.CpValueService;
import com.red.circle.other.infra.enums.user.user.CpApplyStatus;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

class ProcessCpApplyCmdTest {

  @Test
  void agreeBrotherApplyShouldCreateBrotherRelationship() {
    Fixture fixture = new Fixture("BROTHER");

    fixture.exe.execute(processCmd(true));

    verify(fixture.cpRelationshipService).addCp(eq(1001L), eq(2001L),
        nullable(Long.class), eq("LIKEI"), eq("BROTHER"));
    verify(fixture.cpRelationBroadcastMqClient).publish("LIKEI", 3001L, 1001L, 2001L,
        "BROTHER");
    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_ESTABLISHED", "ACCEPTED", true, "BROTHER");
  }

  @Test
  void agreeSistersApplyShouldCreateSistersRelationship() {
    Fixture fixture = new Fixture("SISTERS");

    fixture.exe.execute(processCmd(true));

    verify(fixture.cpRelationshipService).addCp(eq(1001L), eq(2001L),
        nullable(Long.class), eq("LIKEI"), eq("SISTERS"));
    verify(fixture.cpRelationBroadcastMqClient).publish("LIKEI", 3001L, 1001L, 2001L,
        "SISTERS");
    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_ESTABLISHED", "ACCEPTED", true, "SISTERS");
  }

  @Test
  void rejectApplyShouldSendRejectedC2cMessageToSender() {
    Fixture fixture = new Fixture("CP");

    fixture.exe.execute(processCmd(false));

    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_REJECTED", "REJECTED", false, "CP");
  }

  @Test
  void agreeApplyShouldFailWhenPairAlreadyHasAnyRelation() {
    Fixture fixture = new Fixture("BROTHER");
    when(fixture.cpRelationshipService.existsRelationship(1001L, 2001L)).thenReturn(true);

    Assertions.assertThrows(RuntimeException.class, () -> fixture.exe.execute(processCmd(true)));

    verify(fixture.cpApplyService, never()).changeStatusById(eq(3001L),
        any(CpApplyStatus.class));
    verify(fixture.cpRelationshipService, never()).addCp(any(Long.class), any(Long.class),
        nullable(Long.class), any(String.class), any(String.class));
    verify(fixture.cpRelationBroadcastMqClient, never()).publish(any(String.class),
        any(Long.class), any(Long.class), any(Long.class), any(String.class));
    verify(fixture.imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class));
  }

  @Test
  void processOverdueWaitApplyShouldFailStateError() {
    Fixture fixture = new Fixture("CP");
    CpApply overdueApply = apply("CP");
    overdueApply.setCreateTime(Timestamp.valueOf(LocalDateTime.now().minusHours(25)));
    when(fixture.cpApplyService.getById(3001L)).thenReturn(overdueApply);

    Assertions.assertThrows(RuntimeException.class, () -> fixture.exe.execute(processCmd(true)));

    verify(fixture.cpApplyService, never()).changeStatusById(eq(3001L),
        any(CpApplyStatus.class));
    verify(fixture.cpRelationshipService, never()).addCp(any(Long.class), any(Long.class),
        nullable(Long.class), any(String.class), any(String.class));
    verify(fixture.imMessageClient, never()).sendCustomMessage(any(CustomC2cMsgBodyCmd.class));
  }

  @Test
  void agreeApplyShouldAutoRefuseOtherWaitAppliesBetweenSameUsersWithoutRejectedIm() {
    Fixture fixture = new Fixture("CP");
    when(fixture.cpApplyService.refuseOtherWaitAppliesBetweenUsers(3001L, 1001L, 2001L))
        .thenReturn(List.of(apply(3002L, "BROTHER"), apply(3003L, "SISTERS")));

    fixture.exe.execute(processCmd(true));

    verify(fixture.cpApplyService).refuseOtherWaitAppliesBetweenUsers(3001L, 1001L, 2001L);
    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_ESTABLISHED", "ACCEPTED", true, "CP");
  }

  @Test
  void agreeCpApplyShouldRejectPairOtherRelationAppliesAndSenderOtherCpApplies() {
    Fixture fixture = new Fixture("CP");
    fixture.mockProfile(4001L);
    fixture.mockProfile(4002L);
    when(fixture.cpRelationshipService.countCp(1001L, "CP")).thenReturn(0, 1);
    when(fixture.cpRelationshipService.countCp(2001L, "CP")).thenReturn(0, 1);
    when(fixture.cpApplyService.refuseOtherWaitAppliesBetweenUsers(3001L, 1001L, 2001L))
        .thenReturn(List.of(apply(3002L, "BROTHER"), apply(3003L, "SISTERS")));
    when(fixture.cpApplyService.refuseWaitAppliesByUserAndRelation(3001L, 1001L, "CP"))
        .thenReturn(List.of(
            apply(3004L, 1001L, 4001L, "CP"),
            apply(3005L, 1001L, 4002L, "CP")));

    fixture.exe.execute(processCmd(true));

    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_ESTABLISHED", "ACCEPTED", true, "CP");
  }

  @Test
  void agreeCpApplyShouldRejectSenderOtherCpAppliesWhenCpLimitReached() {
    Fixture fixture = new Fixture("CP");
    fixture.mockProfile(4001L);
    fixture.mockProfile(4002L);
    when(fixture.cpRelationshipService.countCp(1001L, "CP")).thenReturn(0, 1);
    when(fixture.cpRelationshipService.countCp(2001L, "CP")).thenReturn(0, 1);
    when(fixture.cpApplyService.refuseWaitAppliesByUserAndRelation(3001L, 1001L, "CP"))
        .thenReturn(List.of(
            apply(3002L, 1001L, 4001L, "CP"),
            apply(3003L, 1001L, 4002L, "CP")));

    fixture.exe.execute(processCmd(true));

    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_ESTABLISHED", "ACCEPTED", true, "CP");
  }

  @Test
  void agreeBrotherApplyShouldNotRejectOtherBrotherAppliesBeforeLimitReached() {
    Fixture fixture = new Fixture("BROTHER");
    when(fixture.cpRelationshipService.countCp(1001L, "BROTHER")).thenReturn(1, 2);
    when(fixture.cpRelationshipService.countCp(2001L, "BROTHER")).thenReturn(0, 1);

    fixture.exe.execute(processCmd(true));

    verify(fixture.cpApplyService, never()).refuseWaitAppliesByUserAndRelation(
        any(Long.class), eq(1001L), eq("BROTHER"));
    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_ESTABLISHED", "ACCEPTED", true, "BROTHER");
  }

  @Test
  void agreeBrotherApplyShouldRejectSenderOtherBrotherAppliesWhenLimitReached() {
    Fixture fixture = new Fixture("BROTHER");
    fixture.mockProfile(4001L);
    fixture.mockProfile(4002L);
    when(fixture.cpRelationshipService.countCp(1001L, "BROTHER")).thenReturn(2, 3);
    when(fixture.cpRelationshipService.countCp(2001L, "BROTHER")).thenReturn(0, 1);
    when(fixture.cpApplyService.refuseWaitAppliesByUserAndRelation(3001L, 1001L, "BROTHER"))
        .thenReturn(List.of(
            apply(3002L, 1001L, 4001L, "BROTHER"),
            apply(3003L, 1001L, 4002L, "BROTHER")));

    fixture.exe.execute(processCmd(true));

    assertProcessMessage(fixture.sentCustomMessage(),
        "RELATION_ESTABLISHED", "ACCEPTED", true, "BROTHER");
  }

  private static ProcessApplyCmd processCmd(boolean agree) {
    ProcessApplyCmd cmd = new ProcessApplyCmd()
        .setApplyId(3001L)
        .setAgree(agree);
    cmd.setReqUserId(2001L);
    cmd.setReqSysOrigin(ReqSysOrigin.of("LIKEI", "LIKEI"));
    return cmd;
  }

  private static CpApply apply(String relationType) {
    return apply(3001L, relationType);
  }

  private static CpApply apply(Long applyId, String relationType) {
    return apply(applyId, 1001L, 2001L, relationType);
  }

  private static CpApply apply(Long applyId, Long sendUserId, Long acceptUserId,
      String relationType) {
    return new CpApply()
        .setId(applyId)
        .setSendApplyUserId(sendUserId)
        .setAcceptApplyUserId(acceptUserId)
        .setRelationType(relationType)
        .setStatus(CpApplyStatus.WAIT.name());
  }

  @SuppressWarnings("unchecked")
  private static void assertProcessMessage(CustomC2cMsgBodyCmd message, String action,
      String status, Boolean agree, String relationType) {
    assertProcessMessage(message, action, status, agree, relationType, 3001L);
  }

  @SuppressWarnings("unchecked")
  private static void assertProcessMessage(CustomC2cMsgBodyCmd message, String action,
      String status, Boolean agree, String relationType, Long applyId) {
    assertProcessMessage(message, action, status, agree, relationType, applyId, "2001", "1001");
  }

  @SuppressWarnings("unchecked")
  private static void assertProcessMessage(CustomC2cMsgBodyCmd message, String action,
      String status, Boolean agree, String relationType, Long applyId, String fromAccount,
      String toAccount) {
    Assertions.assertEquals(fromAccount, message.getFromAccount());
    Assertions.assertEquals(toAccount, message.getToAccount());
    Assertions.assertEquals("CP_BUILD", message.getDesc());
    Map<String, Object> data = (Map<String, Object>) message.getData();
    Assertions.assertEquals("CP_BUILD", data.get("noticeType"));
    Assertions.assertEquals(action, data.get("noticeAction"));
    Assertions.assertEquals(action, data.get("action"));
    Assertions.assertEquals(status, data.get("status"));
    Assertions.assertEquals(status, data.get("result"));
    Assertions.assertEquals(status, data.get("state"));
    Assertions.assertEquals(agree, data.get("agree"));
    Assertions.assertEquals(applyId, data.get("applyId"));
    Assertions.assertEquals(relationType, data.get("relationType"));
    Assertions.assertTrue(data.get("content").toString()
        .contains("\"relationType\":\"" + relationType + "\""));
  }

  private static class Fixture {

    private final CpApplyService cpApplyService = mock(CpApplyService.class);
    private final CpRelationshipService cpRelationshipService = mock(CpRelationshipService.class);
    private final CpRelationBroadcastMqClient cpRelationBroadcastMqClient = mock(
        CpRelationBroadcastMqClient.class);
    private final ImMessageClient imMessageClient = mock(ImMessageClient.class);
    private final UserProfileGateway userProfileGateway = mock(UserProfileGateway.class);
    private final ProcessCpApplyCmd exe;

    private Fixture(String relationType) {
      CpValueService cpValueService = mock(CpValueService.class);
      CpRelationshipCacheService cpRelationshipCacheService = mock(
          CpRelationshipCacheService.class);

      when(cpApplyService.getById(3001L)).thenReturn(apply(relationType));
      when(cpApplyService.changeStatusById(3001L, CpApplyStatus.AGREE)).thenReturn(true);
      when(cpApplyService.changeStatusById(3001L, CpApplyStatus.REFUSE)).thenReturn(true);
      when(cpApplyService.refuseOtherWaitAppliesBetweenUsers(3001L, 1001L, 2001L))
          .thenReturn(List.of());
      when(userProfileGateway.getByUserId(1001L)).thenReturn(profile(1001L));
      when(userProfileGateway.getByUserId(2001L)).thenReturn(profile(2001L));
      when(imMessageClient.sendCustomMessage(any(CustomC2cMsgBodyCmd.class)))
          .thenReturn(ResultResponse.success());

      exe = new ProcessCpApplyCmd(
          cpApplyService,
          cpValueService,
          imMessageClient,
          cpRelationshipService,
          userProfileGateway,
          cpRelationshipCacheService,
          cpRelationBroadcastMqClient);
    }

    private void mockProfile(Long userId) {
      when(userProfileGateway.getByUserId(userId)).thenReturn(profile(userId));
    }

    private CustomC2cMsgBodyCmd sentCustomMessage() {
      ArgumentCaptor<CustomC2cMsgBodyCmd> captor = ArgumentCaptor.forClass(
          CustomC2cMsgBodyCmd.class);
      verify(imMessageClient).sendCustomMessage(captor.capture());
      return captor.getValue();
    }

    private static UserProfile profile(Long userId) {
      UserProfile userProfile = new UserProfile();
      userProfile.setId(userId);
      userProfile.setAccount(userId.toString());
      userProfile.setUserNickname("user-" + userId);
      userProfile.setCountryCode("SA");
      userProfile.setCountryName("Saudi Arabia");
      return userProfile;
    }
  }
}
