package org.initialde.yakasave.Integration;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.transaction.Transactional;
import org.initialde.yakasave.Api.Requests.LoginRequest;
import org.initialde.yakasave.Domain.Entities.Contribution;
import org.initialde.yakasave.Domain.Entities.SavingsFund;
import org.initialde.yakasave.Domain.Entities.WithdrawalApproval;
import org.initialde.yakasave.Domain.Enums.TypeSavingsFund;
import org.initialde.yakasave.Domain.ValueObject.Amount;
import org.initialde.yakasave.Infrastructure.Persistence.SavingsFundRepository;
import org.initialde.yakasave.Infrastructure.Persistence.UserRepository;
import org.initialde.yakasave.Infrastructure.authentication.AuthenticationGateway;
import org.initialde.yakasave.UserFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Transactional
@SpringBootTest
@AutoConfigureMockMvc
public class WithdrawMoneyFromSavingsFundControllerTest {
    private final String ENDPOINT = "/savings-fund/{reference}/withdraw";

    @Autowired
    private SavingsFundRepository savingsFundRepository;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private AuthenticationGateway authenticationGateway;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    public void setup() {
        savingsFundRepository.deleteAll();
        userRepository.deleteAll();
    }

    @Test
    public void shouldWithdrawMoneyFromSavingsFundWhichNotNeedApprovals() throws Exception {
        var reference = UUID.randomUUID().toString();
        var balance = 3000.0;
        final var ownerCredentials = new LoginRequest("owner", "1234");
        var owner = UserFactory.create("owner", "1234");
        userRepository.save(owner);

        var savingsFund = SavingsFund.builder()
                .owner(owner)
                .reference(reference)
                .goalAmount(Amount.MINIMUM_GOAL_AMOUNT)
                .deadline(LocalDate.now().plusDays(1))
                .type(TypeSavingsFund.PERSONAL)
                .balance(Amount.of(balance))
                .contributors(new ArrayList<>(List.of(owner)))
                .build();

        var amount = 1000;

        savingsFundRepository.save(savingsFund);
        String token = authenticationGateway.authenticate(ownerCredentials);
        this.mockMvc.perform(patch(ENDPOINT, reference)
                        .content(objectMapper.writeValueAsString(amount))
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
    }

    @Test
    public void shouldWithdrawMoneyFromSavingsFundWhichNeedApprovals() throws Exception {
        var reference = UUID.randomUUID().toString();
        var balance = 3000.0;

        final var ownerCredentials = new LoginRequest("owner", "1234");
        var owner = UserFactory.create("owner", "1234");
        var contributor1 = UserFactory.create("contributor1", "1234");
        var contributor2 = UserFactory.create("contributor2", "1234");

        var contributionOfContributor1 = new Contribution(contributor1, 1000.0);
        var contributionOfContributor2 = new Contribution(contributor2, 1000.0);
        var contributor1Approval = new WithdrawalApproval(contributor1);
        var contributor2Approval = new WithdrawalApproval(contributor2);

        userRepository.save(owner);
        userRepository.save(contributor1);
        userRepository.save(contributor2);

        var savingsFund = SavingsFund.builder()
                .owner(owner)
                .reference(reference)
                .goalAmount(Amount.MINIMUM_GOAL_AMOUNT)
                .deadline(LocalDate.now().plusDays(1))
                .type(TypeSavingsFund.COLLECTIVE)
                .needsApproval(true)
                .balance(Amount.of(balance))
                .maxAllowedMembers(5)
                .contributors(new ArrayList<>(List.of(owner, contributor1, contributor2)))
                .contributions(new ArrayList<>(List.of(contributionOfContributor1, contributionOfContributor2)))
                .receiptApprovals(new ArrayList<>(List.of(contributor1Approval, contributor2Approval)))
                .build();

        var amount = 2000;

        savingsFundRepository.save(savingsFund);

        String token = authenticationGateway.authenticate(ownerCredentials);
        this.mockMvc.perform(patch(ENDPOINT, reference)
                        .content(objectMapper.writeValueAsString(amount))
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
    }

    @Test
    public void shouldWithdrawMoneyFromSavingsFundWhichNeedsApprovalWhenDoNotHaveHalfApprovals() throws Exception {
        var reference = UUID.randomUUID().toString();
        var balance = 3000.0;

        final var ownerCredentials = new LoginRequest("owner", "1234");
        var owner = UserFactory.create("owner", "1234");
        var contributor1 = UserFactory.create("contributor1", "1234");
        var contributor2 = UserFactory.create("contributor2", "1234");
        var contributor3 = UserFactory.create("contributor2", "1234");

        userRepository.save(owner);
        userRepository.save(contributor1);
        userRepository.save(contributor2);
        userRepository.save(contributor3);

        var savingsFund = SavingsFund.builder()
                .reference(reference).balance(Amount.of(balance))
                .goalAmount(Amount.MINIMUM_GOAL_AMOUNT)
                .deadline(LocalDate.now().plusDays(1))
                .maxAllowedMembers(4)
                .needsApproval(true)
                .type(TypeSavingsFund.COLLECTIVE)
                .contributors(new ArrayList<>(List.of(owner, contributor1, contributor2, contributor3)))
                .receiptApprovals(new ArrayList<>())
                .build();

        var amount = 2000;

        savingsFundRepository.save(savingsFund);

        String token = authenticationGateway.authenticate(ownerCredentials);
        this.mockMvc.perform(patch(ENDPOINT, reference)
                        .content(objectMapper.writeValueAsString(amount))
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isConflict())
                .andReturn();
    }

    @Test
    public void shouldWithdrawMoneyFromSavingsFundWhenAmountWithDrawIsGreaterThanBalance() throws Exception {
        var reference = UUID.randomUUID().toString();
        var balance = 1000.0;

        final var ownerCredentials = new LoginRequest("owner", "1234");
        var owner = UserFactory.create("owner", "1234");
        userRepository.save(owner);

        var savingsFund = SavingsFund.builder()
                .owner(owner)
                .reference(reference).balance(Amount.of(balance))
                .goalAmount(Amount.MINIMUM_GOAL_AMOUNT)
                .deadline(LocalDate.now().plusDays(1))
                .type(TypeSavingsFund.PERSONAL)
                .build();

        var amount = 2000;

        savingsFundRepository.save(savingsFund);

        String token = authenticationGateway.authenticate(ownerCredentials);
        this.mockMvc.perform(patch(ENDPOINT, reference)
                        .content(objectMapper.writeValueAsString(amount))
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isBadRequest())
                .andReturn();
    }

    @Test
    public void shouldWithdrawMoneyFromSavingsFundWhenAmountWithDrawIsLessMinimumAmountAllowed() throws Exception {
        var reference = UUID.randomUUID().toString();
        var balance = 1000.0;

        final var ownerCredentials = new LoginRequest("owner", "1234");
        var owner = UserFactory.create("owner", "1234");
        userRepository.save(owner);

        var savingsFund = SavingsFund.builder()
                .reference(reference).balance(Amount.of(balance))
                .goalAmount(Amount.MINIMUM_GOAL_AMOUNT)
                .deadline(LocalDate.now().plusDays(1))
                .type(TypeSavingsFund.PERSONAL)
                .build();

        var amount = 50;

        savingsFundRepository.save(savingsFund);

        String token = authenticationGateway.authenticate(ownerCredentials);
        this.mockMvc.perform(patch(ENDPOINT, reference)
                        .content(objectMapper.writeValueAsString(amount))
                        .header("Authorization", "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isBadRequest())
                .andReturn();
    }
}
