<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\PersonBundle\Tests\Household;

use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use Chill\PersonBundle\Household\MembersEditorFactory;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * @internal
 *
 * @coversNothing
 */
final class MembersEditorTest extends TestCase
{
    use ProphecyTrait;

    private MembersEditorFactory $factory;

    protected function setUp(): void
    {
        $this->factory = $this->buildMembersEditorFactory();
    }

    public function testAddingParticipationNotSharingHouseholdCloseTheOldOnes()
    {
        $person = new Person();
        $position = (new Position())->setShareHousehold(false);
        $household = new Household();

        // set a first time the person in position
        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household);

        $editor->addMovement($aMonthAgo = new \DateTimeImmutable('1 month ago'), $person, $position);

        // set a second time the person in position
        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household);

        $editor->addMovement($yesterday = new \DateTimeImmutable('yesterday'), $person, $position);

        $this->assertCount(2, $person->getHouseholdParticipationsNotShareHousehold());

        $startDates = [];
        $endDates = [];

        foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) {
            $startDates[] = $participation->getStartDate();
            $endDates[] = $participation->getEndDate();
        }

        $this->assertContains($aMonthAgo, $startDates);
        $this->assertContains($yesterday, $startDates);
        $this->assertContains($yesterday, $endDates);
        $this->assertContains(null, $endDates);
    }

    /**
     * We test that a leave move is possible when member startdate is same as current date.
     */
    public function testLeaveMovementInSameHouseholdFromShareHouseholdToNotShareHouseholdOnSameDate()
    {
        $person = new Person();
        $household = new Household();

        $factory = $this->buildMembersEditorFactory();

        $positionSharing = (new Position())->setShareHousehold(true);
        $positionNotSharing = (new Position())->setShareHousehold(false);

        // create add move
        $editor = $factory->createEditor($household);
        $editor->addMovement(new \DateTimeImmutable('today'), $person, $positionSharing);
        $editor->postMove();

        self::assertContains($person, $household->getCurrentPersons());
        self::assertSame($household, $person->getCurrentHousehold());
        self::assertCount(1, $household->getMembers());

        // create leave move
        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household);
        $editor->addMovement(new \DateTimeImmutable('today'), $person, $positionNotSharing);
        $editor->postMove();

        $participations = $household->getMembers();
        self::assertCount(2, $participations);

        $sharing = $participations->filter(fn (HouseholdMember $hm) => $hm->getShareHousehold());
        self::assertCount(1, $sharing);

        $notSharing = $participations->filter(fn (HouseholdMember $hm) => !$hm->getShareHousehold());
        self::assertCount(1, $notSharing);

        self::assertNotNull($sharing[0]->getEndDate());
        self::assertEquals(new \DateTimeImmutable('today'), $sharing[0]->getEndDate());
    }

    /**
     * We test here a move for a person:.
     *
     * * which was in a position "sharing household"
     * * which move to the another household, in a position "not sharing household"
     *
     * The person should stays in the two households
     */
    public function testMoveFromSharingHouseholdToNotSharingHousehouldInDifferentHousehold()
    {
        $person = new Person();
        $household = new Household();
        $positionSharing = (new Position())->setShareHousehold(true);
        $positionNotSharing = (new Position())->setShareHousehold(false);
        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household);

        // we add the member to the household
        $editor->addMovement(new \DateTimeImmutable('1 month ago'), $person, $positionSharing);

        // double check that the person is in the household
        $this->assertContains($person, $household->getCurrentPersons());

        // we do the move to the position not sharing household
        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldNotBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household2 = new Household());
        $editor->addMovement(new \DateTimeImmutable('yesterday'), $person, $positionNotSharing);
        $editor->postMove();

        $sharings = $household->getCurrentMembers()->filter(static fn (HouseholdMember $m) => $m->getShareHousehold());
        $notSharing = $household2->getCurrentMembers()->filter(static fn (HouseholdMember $m) => !$m->getShareHousehold());

        $this->assertCount(1, $notSharing);
        $this->assertCount(1, $sharings);

        $getPerson = static fn (HouseholdMember $m) => $m->getPerson();

        $this->assertContains($person, $notSharing->map($getPerson));
    }

    /**
     * We test here a move for a person:.
     *
     * * which was in a position "sharing household"
     * * which move to the same household, in a position "not sharing household"
     */
    public function testMoveFromSharingHouseholdToNotSharingHousehouldInSamehouseholdOnDifferentDate()
    {
        $person = new Person();
        $household = new Household();
        $positionSharing = (new Position())->setShareHousehold(true);
        $positionNotSharing = (new Position())->setShareHousehold(false);
        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household);

        // we add the member to the household
        $editor->addMovement(new \DateTimeImmutable('1 month ago'), $person, $positionSharing);

        // double check that the person is in the household
        $this->assertContains($person, $household->getCurrentPersons());

        // we do the move to the position not sharing household
        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household);
        $editor->addMovement(new \DateTimeImmutable('yesterday'), $person, $positionNotSharing);
        $editor->postMove();

        $sharings = $household->getCurrentMembers()->filter(static fn (HouseholdMember $m) => $m->getShareHousehold());
        $notSharing = $household->getCurrentMembers()->filter(static fn (HouseholdMember $m) => !$m->getShareHousehold());

        $this->assertCount(1, $notSharing);
        $this->assertCount(0, $sharings);

        $getPerson = static fn (HouseholdMember $m) => $m->getPerson();

        $this->assertContains($person, $notSharing->map($getPerson));
    }

    /**
     * We test here a move for a person:.
     *
     * * which was in a position "not sharing household"
     * * which move to the same household, in a position "sharing household"
     */
    public function testMoveFromNotSharingHouseholdToSharingHousehouldInSamehousehold()
    {
        $person = new Person();
        $household = new Household();
        $positionSharing = (new Position())->setShareHousehold(true);
        $positionNotSharing = (new Position())->setShareHousehold(false);
        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household);

        // we add the member to the household, at the position "not sharing"
        $editor->addMovement(new \DateTimeImmutable('1 month ago'), $person, $positionNotSharing);

        // double check that the person is in the household
        $this->assertContains($person, $household->getCurrentPersons());

        // we do the move to the position sharing household
        $editor = $factory->createEditor($household);
        $editor->addMovement(new \DateTimeImmutable('yesterday'), $person, $positionSharing);

        self::assertCount(2, $household->getMembers());

        $sharings = $household->getCurrentMembers()->filter(static fn (HouseholdMember $m) => $m->getShareHousehold());
        $notSharing = $household->getCurrentMembers()->filter(static fn (HouseholdMember $m) => !$m->getShareHousehold());

        $this->assertCount(0, $notSharing);
        $this->assertCount(1, $sharings);

        $getPerson = static fn (HouseholdMember $m) => $m->getPerson();

        $this->assertContains($person, $sharings->map($getPerson));
        $this->assertNotContains($person, $notSharing->map($getPerson));
    }

    /**
     * We test here a move for a person:.
     *
     * * which was in a position "not sharing household"
     * * which move to the same household, in a position "sharing household"
     */
    public function testMoveFromNotSharingHouseholdToSharingHousehouldInSamehouseholdOnSameDate()
    {
        $person = new Person();
        $household = new Household();
        $positionSharing = (new Position())->setShareHousehold(true);
        $positionNotSharing = (new Position())->setShareHousehold(false);
        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household);

        // we add the member to the household, at the position "not sharing"
        $editor->addMovement(new \DateTimeImmutable('today'), $person, $positionNotSharing);

        // double check that the person is in the household
        $this->assertContains($person, $household->getCurrentPersons());

        // we do the move to the position sharing household
        $editor = $factory->createEditor($household);
        $editor->addMovement(new \DateTimeImmutable('today'), $person, $positionSharing);

        self::assertCount(2, $household->getMembers());

        $sharings = $household->getCurrentMembers()->filter(static fn (HouseholdMember $m) => $m->getShareHousehold());
        $notSharing = $household->getCurrentMembers()->filter(static fn (HouseholdMember $m) => !$m->getShareHousehold());

        $this->assertCount(0, $notSharing);
        $this->assertCount(1, $sharings);

        $getPerson = static fn (HouseholdMember $m) => $m->getPerson();

        $this->assertContains($person, $sharings->map($getPerson));
        $this->assertNotContains($person, $notSharing->map($getPerson));
    }

    public function testMovePersonWithoutSharedHousehold()
    {
        $person = new Person();
        $position = (new Position())
            ->setShareHousehold(false);
        $household1 = new Household();
        $household2 = new Household();
        $editor = $this->factory->createEditor($household1);

        $editor->addMovement(
            \DateTimeImmutable::createFromFormat('Y-m-d', '2020-01-01'),
            $person,
            $position
        );

        $persistables = $editor->getPersistable();
        $this->assertEquals(1, \count($persistables));

        $membership1 = $person->getHouseholdParticipations()->first();
        $this->assertSame($household1, $membership1->getHousehold());
        $this->assertNull($membership1->getEndDate());

        // move to another household
        $date = \DateTimeImmutable::createFromFormat('Y-m-d', '2021-01-01');
        $editor = $this->factory->createEditor($household2);
        $editor->addMovement(
            $date,
            $person,
            $position
        );

        $persistables = $editor->getPersistable();
        $this->assertEquals(1, \count($persistables));

        $membership2 = $person->getHouseholdParticipations()->last();
        $this->assertNull($membership2->getEndDate());
        $this->assertSame($household2, $membership2->getHousehold());
        $this->assertNull(
            $membership1->getEndDate(),
            'assert that the membership1 is not closed'
        );
    }

    public function testMovePersonWithSharedHousehold()
    {
        $person = new Person();
        $position = (new Position())
            ->setShareHousehold(true);
        $household1 = new Household();
        $household2 = new Household();
        $editor = $this->factory->createEditor($household1);

        $editor->addMovement(
            \DateTimeImmutable::createFromFormat('Y-m-d', '2020-01-01'),
            $person,
            $position
        );

        $persistables = $editor->getPersistable();
        $this->assertEquals(\count($persistables), 1);

        $membership1 = $persistables[0];
        $this->assertSame($household1, $membership1->getHousehold());
        $this->assertNull($membership1->getEndDate());

        // move to another household
        $date = \DateTimeImmutable::createFromFormat('Y-m-d', '2021-01-01');
        $editor = $this->factory->createEditor($household2);
        $editor->addMovement(
            $date,
            $person,
            $position
        );

        $persistables = $editor->getPersistable();
        $this->assertEquals(1, \count($persistables));

        $membership2 = $persistables[0];
        $this->assertSame($household2, $membership2->getHousehold());
        $this->assertNull($membership2->getEndDate());
        $this->assertNotNull(
            $membership1->getEndDate(),
            'assert that the membership1 is closed'
        );
        $this->assertEquals($date, $membership1->getEndDate());
    }

    public function testPostMoveToAPositionNotSharingHouseholdOnSameDay()
    {
        $person = new Person();
        $positionNotSharing = (new Position())
            ->setShareHousehold(false);
        $positionSharing = (new Position())->setShareHousehold(true);
        $household1 = new Household();
        $household2 = new Household();

        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household1);
        $editor->addMovement(new \DateTimeImmutable('today'), $person, $positionSharing);
        $editor->postMove();

        self::assertContains($person, $household1->getCurrentPersons());
        self::assertContains($person, $household1->getCurrentMembers()
            ->filter(fn (HouseholdMember $m) => $m->getShareHousehold())
            ->map(fn (HouseholdMember $m) => $m->getPerson()));
        self::assertSame($household1, $person->getCurrentHousehold());

        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldNotBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household2);

        $editor->addMovement(new \DateTimeImmutable('today'), $person, $positionNotSharing);
        $editor->postMove();

        // $household1 still contains $person
        self::assertContains($person, $household1->getCurrentPersons());
        self::assertContains($person, $household1->getCurrentMembers()
            ->filter(fn (HouseholdMember $m) => $m->getShareHousehold())
            ->map(fn (HouseholdMember $m) => $m->getPerson()));
        self::assertSame($household1, $person->getCurrentHousehold());

        // $household2 contains $person at non-sharing position
        self::assertContains($person, $household2->getCurrentMembers()
            ->filter(fn (HouseholdMember $m) => !$m->getShareHousehold())
            ->map(fn (HouseholdMember $m) => $m->getPerson()));
        self::assertContains(
            $household2,
            $person->getHouseholdParticipationsNotShareHousehold()
                ->map(fn (HouseholdMember $hm) => $hm->getHousehold())
        );
    }

    public function testPostMoveToAPositionNotSharingHouseholdOnDifferentDays()
    {
        $person = new Person();
        $positionNotSharing = (new Position())
            ->setShareHousehold(false);
        $positionSharing = (new Position())->setShareHousehold(true);
        $household1 = new Household();
        $household2 = new Household();

        $factory = $this->buildMembersEditorFactory();
        $editor = $factory->createEditor($household1);
        $editor->addMovement(new \DateTimeImmutable('1 year ago'), $person, $positionSharing);
        $editor->postMove();

        self::assertContains($person, $household1->getCurrentPersons());
        self::assertContains($person, $household1->getCurrentMembers()
            ->filter(fn (HouseholdMember $m) => $m->getShareHousehold())
            ->map(fn (HouseholdMember $m) => $m->getPerson()));
        self::assertSame($household1, $person->getCurrentHousehold());

        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldNotBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household2);

        $editor->addMovement(new \DateTimeImmutable('yesterday'), $person, $positionNotSharing);
        $editor->postMove();

        // $household1 still contains $person
        self::assertContains($person, $household1->getCurrentPersons());
        self::assertContains($person, $household1->getCurrentMembers()
            ->filter(fn (HouseholdMember $m) => $m->getShareHousehold())
            ->map(fn (HouseholdMember $m) => $m->getPerson()));
        self::assertSame($household1, $person->getCurrentHousehold());

        // $household2 contains $person at non-sharing position
        self::assertContains($person, $household2->getCurrentMembers()
            ->filter(fn (HouseholdMember $m) => !$m->getShareHousehold())
            ->map(fn (HouseholdMember $m) => $m->getPerson()));
        self::assertContains(
            $household2,
            $person->getHouseholdParticipationsNotShareHousehold()
                ->map(fn (HouseholdMember $hm) => $hm->getHousehold())
        );
    }

    public function testPostMoveToAPositionSharingHouseholdAndSameHousehold()
    {
        $person = new Person();
        $position = (new Position())
            ->setShareHousehold(true);
        $position2 = (new Position())
            ->setShareHousehold(true);
        $household1 = new Household();

        // set into the first household
        $editor = $this->buildMembersEditorFactory()
            ->createEditor($household1);
        $editor->addMovement(new \DateTimeImmutable('1 year ago'), $person, $position);

        // prepare for next move
        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldNotBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household1);

        $editor->addMovement(new \DateTimeImmutable('now'), $person, $position2);

        $editor->postMove();
    }

    public function testPostMoveToAPositionSharingHouseholdFromDifferentHousehold()
    {
        $person = new Person();
        $position = (new Position())
            ->setShareHousehold(true);
        $household1 = new Household();
        $household2 = new Household();

        // set into the first household
        $editor = $this->buildMembersEditorFactory()
            ->createEditor($household1);
        $editor->addMovement(new \DateTimeImmutable('1 year ago'), $person, $position);

        // perform now the movement
        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household2);

        $editor->addMovement(new \DateTimeImmutable('now'), $person, $position);

        $editor->postMove();
    }

    public function testPostMoveToAPositionSharingHouseholdFromNoHousehold()
    {
        $person = new Person();
        $position = (new Position())
            ->setShareHousehold(true);
        $household1 = new Household();
        $eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
        $eventDispatcher
            ->dispatch(Argument::type(PersonAddressMoveEvent::class))
            ->shouldBeCalled();
        $factory = $this->buildMembersEditorFactory(
            $eventDispatcher->reveal(),
            null
        );
        $editor = $factory->createEditor($household1);

        $editor->addMovement(new \DateTimeImmutable('now'), $person, $position);

        $editor->postMove();
    }

    private function buildMembersEditorFactory(
        ?EventDispatcherInterface $eventDispatcher = null,
        ?ValidatorInterface $validator = null,
    ) {
        if (null === $eventDispatcher) {
            $double = $this->getProphet()->prophesize();
            $double->willImplement(EventDispatcherInterface::class);
            $double->dispatch(Argument::type(PersonAddressMoveEvent::class));
            $eventDispatcher = $double->reveal();
        }

        if (null === $validator) {
            $double = $this->getProphet()->prophesize();
            $double->willImplement(ValidatorInterface::class);
            $validator = $double->reveal();
        }

        return new MembersEditorFactory(
            $eventDispatcher,
            $validator
        );
    }
}
