<?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\Controller;

use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Test\PrepareClientTrait;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;

/**
 * Test api for AccompanyingCourseControllerTest.
 *
 * @internal
 *
 * @coversNothing
 */
final class AccompanyingCourseApiControllerTest extends WebTestCase
{
    use PrepareClientTrait;

    private static EntityManagerInterface $em;

    private ?AccompanyingPeriod $period = null;

    private ?int $periodId = null;

    private ?int $personId = null;

    protected function tearDown(): void
    {
        self::ensureKernelShutdown();
    }

    /**
     * @dataProvider dataGenerateRandomAccompanyingCourse
     */
    public function testAccompanyingCourseAddParticipation(int $personId, int $periodId)
    {
        $this->markTestIncomplete('fix test with validation');
        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $periodId),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'person', 'id' => $personId], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $this->assertArrayHasKey('id', $data);
        $this->assertArrayHasKey('startDate', $data);
        $this->assertNotNull($data['startDate']);

        // check by deownloading the accompanying cours

        $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId));
        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), null, 512, JSON_THROW_ON_ERROR);

        // check that the person id is contained
        $participationsPersonsIds = \array_map(
            static fn ($participation) => $participation->person->id,
            $data->participations
        );

        $this->assertContains($personId, $participationsPersonsIds);

        // check removing the participation
        $this->client->request(
            Request::METHOD_DELETE,
            sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $periodId),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'person', 'id' => $personId], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $this->assertArrayHasKey('id', $data);
        $this->assertArrayHasKey('startDate', $data);
        $this->assertNotNull($data['startDate']);
        $this->assertArrayHasKey('endDate', $data);
        $this->assertNotNull($data['endDate']);
    }

    /**
     * @dataProvider dataGenerateRandomAccompanyingCourseWithSocialIssue
     */
    public function testAccompanyingCourseAddRemoveSocialIssue(AccompanyingPeriod $period, SocialIssue $si)
    {
        $this->markTestIncomplete('fix test with validation');
        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/socialissue.json', $period->getId()),
            [],
            [],
            [],
            \json_encode(['type' => 'social_issue', 'id' => $si->getId()], JSON_THROW_ON_ERROR)
        );

        $this->assertTrue(\in_array($this->client->getResponse()->getStatusCode(), [200, 422], true));

        if (422 === $this->client->getResponse()->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $data = \json_decode((string) $this->client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR);
        $this->assertArrayHasKey('id', $data);
        $this->assertArrayHasKey('type', $data);
        $this->assertEquals('social_issue', $data['type']);

        $this->client->request(
            Request::METHOD_DELETE,
            sprintf('/api/1.0/person/accompanying-course/%d/socialissue.json', $period->getId()),
            [],
            [],
            [],
            \json_encode(['type' => 'social_issue', 'id' => $si->getId()], JSON_THROW_ON_ERROR)
        );

        $this->assertTrue(\in_array($this->client->getResponse()->getStatusCode(), [200, 422], true));
    }

    public static function dataGenerateRandomAccompanyingCourseWithSocialIssue()
    {
        // note about max result for person query, and maxGenerated:
        //
        // in the final loop, an id is popped out of the personIds array twice:
        //
        // * one for getting the person, which will in turn provide his accompanying period;
        // * one for getting the personId to populate to the data manager
        //
        // Ensure to keep always $maxGenerated to the double of $maxResults. x8 is a good compromize :)
        $maxGenerated = 3;
        $maxResults = $maxGenerated * 8;

        self::bootKernel();
        $em = self::getContainer()->get(EntityManagerInterface::class);
        $center = $em->getRepository(Center::class)
            ->findOneBy(['name' => 'Center A']);
        $qb = $em->createQueryBuilder();

        $personIds = $qb
            ->select('p.id')
            ->distinct(true)
            ->from(Person::class, 'p')
            ->join('p.accompanyingPeriodParticipations', 'participation')
            ->join('participation.accompanyingPeriod', 'ap')
            ->join('p.centerCurrent', 'cc')
            ->where(
                $qb->expr()->eq(
                    'cc.center',
                    ':center'
                )
            )
            ->andWhere(
                $qb->expr()->gt(
                    'SIZE(p.accompanyingPeriodParticipations)',
                    0
                )
            )
            ->andWhere(
                $qb->expr()->eq('ap.step', ':step')
            )
            ->setParameter('center', $center)
            ->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
            ->setMaxResults($maxResults)
            ->getQuery()
            ->getScalarResult();

        // create a random order
        shuffle($personIds);

        $socialIssues = $em->createQuery('SELECT s FROM '.
            SocialIssue::class.' s ')
            ->setMaxResults(10)
            ->getResult();

        $nbGenerated = 0;

        while ($nbGenerated < $maxGenerated) {
            $id = \array_pop($personIds)['id'];

            $person = $em->getRepository(Person::class)
                ->find($id);
            $periods = $person->getAccompanyingPeriods();

            yield [$periods[\array_rand($periods)], $socialIssues[\array_rand($socialIssues)]];

            ++$nbGenerated;
        }

        self::ensureKernelShutdown();
    }

    /**
     * @dataProvider dataGenerateRandomAccompanyingCourse
     */
    public function testAccompanyingCourseShow(int $personId, int $periodId)
    {
        $client = $this->getClientAuthenticated();
        $client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId));
        $response = $client->getResponse();

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $data = \json_decode((string) $response->getContent(), null, 512, JSON_THROW_ON_ERROR);
        $this->assertEquals(
            $data->id,
            $periodId,
            "test that the response's data contains the id of the period"
        );
        $this->assertGreaterThan(0, $data->participations);
    }

    /**
     * @dataProvider dataGenerateRandomAccompanyingCourse
     */
    public function testAccompanyingPeriodPatch(int $personId, int $periodId)
    {
        $this->markTestIncomplete('fix test with validation');
        $period = self::getContainer()->get(AccompanyingPeriodRepository::class)
            ->find($periodId);
        $initialValueEmergency = $period->isEmergency();
        $em = self::getContainer()->get(EntityManagerInterface::class);

        $this->client->request(
            Request::METHOD_PATCH,
            sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'accompanying_period', 'emergency' => !$initialValueEmergency], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $period = $em->getRepository(AccompanyingPeriod::class)
            ->find($periodId);
        $this->assertEquals(!$initialValueEmergency, $period->isEmergency());

        // restore the initial value
        $period->setEmergency($initialValueEmergency);
        $em->flush();
    }

    /**
     * @dataProvider dataGenerateRandomRequestorValidData
     */
    public function testCommentWithValidData(AccompanyingPeriod $period, mixed $personId, mixed $thirdPartyId)
    {
        $this->markTestIncomplete('fix test with validation');
        $em = self::getContainer()->get(EntityManagerInterface::class);

        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/comment.json', $period->getId()),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'accompanying_period_comment', 'content' => 'this is a text'])
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $this->assertArrayHasKey('id', $data);

        $this->client->request(
            Request::METHOD_DELETE,
            sprintf('/api/1.0/person/accompanying-course/%d/comment.json', $period->getId()),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'accompanying_period_comment', 'id' => $data['id']], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));
    }

    /**
     * @dataProvider dataGenerateNewAccompanyingCourse
     */
    public function testConfirm(AccompanyingPeriod $period)
    {
        $this->markTestIncomplete('fix test with validation');
        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/confirm.json', $period->getId())
        );

        $this->assertTrue(\in_array($this->client->getResponse()->getStatusCode(), [200, 422], true));

        if (422 === $this->client->getResponse()->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        // add period to remove it in tear down
        $this->period = $period;
    }

    public static function dataGenerateNewAccompanyingCourse()
    {
        self::bootKernel();
        $em = self::getContainer()->get(EntityManagerInterface::class);

        $period = new AccompanyingPeriod(new \DateTime('1 week ago'));
        $user = $em->getRepository(User::class)
            ->findOneByUsernameCanonical('center a_social');
        $period->setCreatedBy($user);
        // $period->setCreatedAt(new \DateTime('yesterday'));

        $center = $em->getRepository(Center::class)
            ->findOneBy(['name' => 'Center A']);

        $personIds = $em->createQuery('SELECT p.id FROM '.
            Person::class.' p JOIN p.centerCurrent cc'.
            ' WHERE cc.center = :center')
            ->setParameter('center', $center)
            ->setMaxResults(100)
            ->getScalarResult();

        // create a random order
        shuffle($personIds);

        for ($i = 0; 2 > $i; ++$i) {
            $person = $em->getRepository(Person::class)->find(\array_pop($personIds));
            $period->addPerson($person);
        }

        $em->persist($period);
        $em->flush();

        yield [$period];

        self::ensureKernelShutdown();
    }

    /**
     * @dataProvider dataGenerateRandomAccompanyingCourse
     */
    public function testReferralAvailable(int $personId, int $periodId)
    {
        $client = $this->getClientAuthenticated();
        $client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/referrers-suggested.json', $periodId)
        );

        $this->assertTrue(\in_array($client->getResponse()->getStatusCode(), [200, 422], true));
    }

    public static function dataGenerateRandomAccompanyingCourse()
    {
        // note about max result for person query, and maxGenerated:
        //
        // in the final loop, an id is popped out of the personIds array twice:
        //
        // * one for getting the person, which will in turn provide his accompanying period;
        // * one for getting the personId to populate to the data manager
        //
        // Ensure to keep always $maxGenerated to the double of $maxResults. x8 is a good compromize :)
        $maxGenerated = 3;
        $maxResults = $maxGenerated * 8;

        self::bootKernel();
        $em = self::getContainer()->get(EntityManagerInterface::class);
        $center = $em->getRepository(Center::class)
            ->findOneBy(['name' => 'Center A']);

        $qb = $em->createQueryBuilder();
        $personIds = $qb
            ->select('p.id')
            ->distinct(true)
            ->from(Person::class, 'p')
            ->join('p.accompanyingPeriodParticipations', 'participation')
            ->join('participation.accompanyingPeriod', 'ap')
            ->join('p.centerCurrent', 'cc')
            ->where(
                $qb->expr()->eq(
                    'cc.center',
                    ':center'
                )
            )
            ->andWhere(
                $qb->expr()->gt(
                    'SIZE(p.accompanyingPeriodParticipations)',
                    0
                )
            )
            ->andWhere(
                $qb->expr()->eq('ap.step', ':step')
            )
            ->setParameter('center', $center)
            ->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
            ->setMaxResults($maxResults)
            ->getQuery()
            ->getScalarResult();

        // create a random order
        shuffle($personIds);

        $nbGenerated = 0;

        while ($nbGenerated < $maxGenerated) {
            $id = \array_pop($personIds)['id'];

            $person = $em->getRepository(Person::class)
                ->find($id);
            $periods = $person->getAccompanyingPeriods();

            yield [\array_pop($personIds)['id'], $periods[\array_rand($periods)]->getId()];

            ++$nbGenerated;
        }

        self::ensureKernelShutdown();
    }

    /**
     * @dataProvider dataGenerateRandomRequestorValidData
     */
    public function testRequestorWithValidData(AccompanyingPeriod $period, mixed $personId, mixed $thirdPartyId)
    {
        $this->markTestIncomplete('fix test with validation');
        $em = self::getContainer()->get(EntityManagerInterface::class);

        // post a person
        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'person', 'id' => $personId], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $this->assertArrayHasKey('id', $data);
        $this->assertEquals($personId, $data['id']);

        // check into database
        $period = $em->getRepository(AccompanyingPeriod::class)
            ->find($period->getId());
        $this->assertInstanceOf(Person::class, $period->getRequestor());
        $this->assertEquals($personId, $period->getRequestor()->getId());

        // post a third party
        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'thirdparty', 'id' => $thirdPartyId], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $this->assertArrayHasKey('id', $data);
        $this->assertEquals($thirdPartyId, $data['id']);

        // check into database
        $period = $em->getRepository(AccompanyingPeriod::class)
            ->find($period->getId());
        $this->assertInstanceOf(ThirdParty::class, $period->getRequestor());
        $this->assertEquals($thirdPartyId, $period->getRequestor()->getId());

        // remove the requestor
        $this->client->request(
            Request::METHOD_DELETE,
            sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId())
        );
        $response = $this->client->getResponse();
        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        // check into database
        $period = $em->getRepository(AccompanyingPeriod::class)
            ->find($period->getId());
        $em->refresh($period);
        $this->assertNull($period->getRequestor());
    }

    /**
     * @dataProvider dataGenerateRandomRequestorValidData
     */
    public function testResourceWithValidData(AccompanyingPeriod $period, mixed $personId, mixed $thirdPartyId)
    {
        $this->markTestIncomplete('fix test with validation');
        $em = self::getContainer()->get(EntityManagerInterface::class);

        // post a person
        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'accompanying_period_resource', 'resource' => ['type' => 'person', 'id' => $personId]])
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $this->assertArrayHasKey('id', $data);
        $this->assertEquals($personId, $data['resource']['id']);

        // check into database
        $resource = $em->getRepository(Resource::class)
            ->find($data['id']);
        $this->assertInstanceOf(Resource::class, $resource);
        $this->assertInstanceOf(Person::class, $resource->getResource());
        $this->assertEquals($personId, $resource->getResource()->getId());

        // remove the resource
        $this->client->request(
            Request::METHOD_DELETE,
            sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
            [],
            [],
            [], // server
            \json_encode(['type' => 'accompanying_period_resource', 'id' => $resource->getId()], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();
        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        // post a third party
        $this->client->request(
            Request::METHOD_POST,
            sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()),
            [], // parameters
            [], // files
            [], // server parameters
            \json_encode(['type' => 'accompanying_period_resource', 'resource' => ['type' => 'thirdparty', 'id' => $thirdPartyId]])
        );
        $response = $this->client->getResponse();
        $data = \json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);

        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));

        if (422 === $response->getStatusCode()) {
            $this->markTestSkipped('the next tests should appears only on valid accompanying period');
        }

        $this->assertArrayHasKey('id', $data);
        $this->assertEquals($thirdPartyId, $data['resource']['id']);

        // check into database
        $resource = $em->getRepository(Resource::class)
            ->find($data['id']);
        $this->assertInstanceOf(Resource::class, $resource);
        $this->assertInstanceOf(ThirdParty::class, $resource->getResource());
        $this->assertEquals($thirdPartyId, $resource->getResource()->getId());

        // remove the resource
        $this->client->request(
            Request::METHOD_DELETE,
            sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
            [],
            [],
            [], // server
            \json_encode(['type' => 'accompanying_period_resource', 'id' => $resource->getId()], JSON_THROW_ON_ERROR)
        );
        $response = $this->client->getResponse();
        $this->assertTrue(\in_array($response->getStatusCode(), [200, 422], true));
    }

    public static function dataGenerateRandomRequestorValidData(): \Iterator
    {
        $dataLength = 2;
        $maxResults = 100;

        self::bootKernel();
        $em = self::getContainer()->get(EntityManagerInterface::class);
        $center = $em->getRepository(Center::class)
            ->findOneBy(['name' => 'Center A']);
        $qb = $em->createQueryBuilder();

        $personIds = $qb
            ->select('p.id')
            ->distinct(true)
            ->from(Person::class, 'p')
            ->join('p.accompanyingPeriodParticipations', 'participation')
            ->join('participation.accompanyingPeriod', 'ap')
            ->join('p.centerCurrent', 'cc')
            ->where(
                $qb->expr()->eq(
                    'cc.center',
                    ':center'
                )
            )
            ->andWhere(
                $qb->expr()->gt(
                    'SIZE(p.accompanyingPeriodParticipations)',
                    0
                )
            )
            ->andWhere(
                $qb->expr()->eq('ap.step', ':step')
            )
            ->setParameter('center', $center)
            ->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
            ->setMaxResults($maxResults)
            ->getQuery()
            ->getScalarResult();

        // create a random order
        shuffle($personIds);

        $thirdPartyIds = $em->createQuery('SELECT t.id FROM '.
            ThirdParty::class.' t ')
            ->setMaxResults($maxResults)
            ->getScalarResult();

        // create a random order
        shuffle($thirdPartyIds);

        $i = 0;

        while ($i <= $dataLength) {
            $person = $em->getRepository(Person::class)
                ->find(\array_pop($personIds)['id']);

            if (0 === \count($person->getAccompanyingPeriods())) {
                continue;
            }

            $period = $person->getAccompanyingPeriods()[0];

            yield [$period, \array_pop($personIds)['id'], \array_pop($thirdPartyIds)['id']];
            ++$i;
        }

        self::ensureKernelShutdown();
    }

    public function testShow404()
    {
        $client = $this->getClientAuthenticated();
        $client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', 99999));
        $response = $client->getResponse();

        $this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)");
    }
}
