package org.initialde.yakasave.Unit;

import org.initialde.yakasave.Application.WithdrawMoneyFromSavingsFund;
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.Exceptions.AmountWithDrawGreaterThanBalanceException;
import org.initialde.yakasave.Domain.Exceptions.AmountWithDrawLessThanMinimumAuthorizedException;
import org.initialde.yakasave.Domain.Exceptions.NotAuthorizeToWithdrawFromSavingsFundException;
import org.initialde.yakasave.Domain.ValueObject.Amount;
import org.initialde.yakasave.Infrastructure.Persistence.Fake.InMemorySavingsFundRepository;
import org.initialde.yakasave.Infrastructure.authentication.FakeAuthenticationGateway;
import org.initialde.yakasave.Infrastructure.authentication.Jwt.fake.FakeJwtService;
import org.initialde.yakasave.UserFactory;
import org.junit.jupiter.api.Test;

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

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

public class WithdrawMoneyFromSavingsFunTests {
    @Test
    public void shouldWithdrawMoneyFromSavingsFunWhichNotNeedApprovals() {
        var reference = UUID.randomUUID().toString();
        var owner = UserFactory.create("user", "password");
        var balance = 3000.0;
        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 amountWithdraw = 2000.0;
        var tokenGenerator = new FakeJwtService();

        var authenticationGateway = new FakeAuthenticationGateway(tokenGenerator);
        authenticationGateway.authenticate(owner);

        var savingsFundRepository = new InMemorySavingsFundRepository();
        savingsFundRepository.save(savingsFund);

        var withdrawMoneyFromSavingsFun = new WithdrawMoneyFromSavingsFund(savingsFundRepository, authenticationGateway);
        withdrawMoneyFromSavingsFun.withDrawMoney(savingsFund, amountWithdraw);

        var actualWithdrawMoney = savingsFundRepository.findByReference(reference).get().getBalance();

        assertEquals(balance - amountWithdraw, actualWithdrawMoney.toDouble());
    }

    @Test
    public void shouldWithdrawMoneyFromSavingsFunWhichNeedsApproval() {
        var reference = UUID.randomUUID().toString();
        var owner = UserFactory.create( "owner", "1234");
        var balance = 3000.0;
        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);

        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)))
                .contributions(new ArrayList<>(List.of(contributionOfContributor1, contributionOfContributor2)))
                .receiptApprovals(new ArrayList<>(List.of(contributor1Approval, contributor2Approval)))
                .build();
        var amountWithdraw = 2000.0;
        var tokenGenerator = new FakeJwtService();

        var authenticationGateway = new FakeAuthenticationGateway(tokenGenerator);
        authenticationGateway.authenticate(owner);

        var savingsFundRepository = new InMemorySavingsFundRepository();
        savingsFundRepository.save(savingsFund);

        var withdrawMoneyFromSavingsFun = new WithdrawMoneyFromSavingsFund(savingsFundRepository, authenticationGateway);
        withdrawMoneyFromSavingsFun.withDrawMoney(savingsFund, amountWithdraw);

        var actualWithDrawMoney = savingsFundRepository.findByReference(reference).get().getBalance();

        assertEquals(balance - amountWithdraw, actualWithDrawMoney.toDouble());
    }

    @Test
    public void shouldFailWithdrawMoneyFromSavingsFundWhichNeedsApprovalWhenDoNotHaveHalfApprovals() {
        var reference = UUID.randomUUID().toString();
        var owner = UserFactory.create( "owner", "1234");
        var balance = 3000.0;
        var contributor1 = UserFactory.create("contributor1", "1234");
        var contributor2 = UserFactory.create( "contributor2", "1234");
        var contributor3 = UserFactory.create( "contributor2", "1234");

        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 amountWithdraw = 2000.0;
        var tokenGenerator = new FakeJwtService();

        var authenticationGateway = new FakeAuthenticationGateway(tokenGenerator);
        authenticationGateway.authenticate(owner);

        var savingsFundRepository = new InMemorySavingsFundRepository();
        savingsFundRepository.save(savingsFund);

        var withdrawMoneyFromSavingsFun = new WithdrawMoneyFromSavingsFund(savingsFundRepository, authenticationGateway);

        assertThrowsExactly(NotAuthorizeToWithdrawFromSavingsFundException.class, () -> withdrawMoneyFromSavingsFun.withDrawMoney(savingsFund, amountWithdraw));
    }

    @Test
    public void shouldFailWithdrawMoneyFromSavingsFunWhenAmountWithDrawIsGreaterThanBalance() {
        var reference = UUID.randomUUID().toString();
        var owner = UserFactory.create("user", "password");
        var balance = 1000.0;
        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 amountWithDraw = 2000.0;
        var tokenGenerator = new FakeJwtService();

        var authenticationGateway = new FakeAuthenticationGateway(tokenGenerator);
        authenticationGateway.authenticate(owner);

        var savingsFundRepository = new InMemorySavingsFundRepository();
        savingsFundRepository.save(savingsFund);

        var withdrawMoneyFromSavingsFun = new WithdrawMoneyFromSavingsFund(savingsFundRepository, authenticationGateway);

        assertThrowsExactly(AmountWithDrawGreaterThanBalanceException.class, () -> withdrawMoneyFromSavingsFun.withDrawMoney(savingsFund, amountWithDraw));
    }

    @Test
    public void shouldFailToWithdrawMoneyFromSavingsFunWhenAmountWithDrawIsLessMinimumAmountAllowed() {
        var reference = UUID.randomUUID().toString();
        var owner = UserFactory.create("user", "password");
        var balance = 1000.0;
        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 amountWithDraw = 50.0;
        var tokenGenerator = new FakeJwtService();

        var authenticationGateway = new FakeAuthenticationGateway(tokenGenerator);
        authenticationGateway.authenticate(owner);

        var savingsFundRepository = new InMemorySavingsFundRepository();
        savingsFundRepository.save(savingsFund);

        var withDrawMoneyFromSavingsFun = new WithdrawMoneyFromSavingsFund(savingsFundRepository, authenticationGateway);

        assertThrowsExactly(AmountWithDrawLessThanMinimumAuthorizedException.class, () -> withDrawMoneyFromSavingsFun.withDrawMoney(savingsFund, amountWithDraw));
    }
}
