<?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\TicketBundle\Tests\Serializer\Normalizer;

use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\PersonBundle\Entity\Person;
use Chill\TicketBundle\Entity\AddresseeHistory;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Comment;
use Chill\TicketBundle\Entity\EmergencyStatusEnum;
use Chill\TicketBundle\Entity\EmergencyStatusHistory;
use Chill\TicketBundle\Entity\Motive;
use Chill\TicketBundle\Entity\MotiveHistory;
use Chill\TicketBundle\Entity\PersonHistory;
use Chill\TicketBundle\Entity\StateEnum;
use Chill\TicketBundle\Entity\StateHistory;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Security\Voter\CommentVoter;
use Chill\TicketBundle\Serializer\Normalizer\TicketNormalizer;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
 * @internal
 *
 * @coversNothing
 */
class TicketNormalizerTest extends KernelTestCase
{
    use ProphecyTrait;

    private ObjectProphecy $security;

    protected function setUp(): void
    {
        $this->security = $this->prophesize(Security::class);
    }

    /**
     * @dataProvider provideTickets
     */
    public function testNormalize(Ticket $ticket, array $expected): void
    {
        $actual = $this->buildNormalizer()->normalize($ticket, 'json', ['groups' => 'read']);
        self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));

        foreach (array_keys($expected) as $k) {
            if ('history' === $k) {
                continue;
            }
            self::assertEqualsCanonicalizing($expected[$k], $actual[$k], sprintf("assert the content of the '%s' key", $k));
        }

        self::assertArrayHasKey('history', $actual);
        self::assertIsArray($actual['history']);

        foreach ($actual['history'] as $k => $eventType) {
            self::assertEquals($expected['history'][$k]['event_type'], $eventType['event_type']);
        }
    }

    public static function provideTickets(): iterable
    {
        $t = new Ticket();

        // added by action
        new StateHistory(StateEnum::OPEN, $t, new \DateTimeImmutable('2024-06-16T00:00:00Z'));

        // those are added by doctrine listeners
        $t->setCreatedAt(new \DateTimeImmutable('2024-06-16T00:00:00Z'));
        $t->setCreatedBy(new User());
        $t->setUpdatedAt(new \DateTimeImmutable('2024-06-16T00:00:00Z'));
        $t->setUpdatedBy(new User());

        yield [
            // this a nearly empty ticket
            $t,
            [
                'type' => 'ticket_ticket',
                'type_extended' => 'ticket_ticket:extended',
                'createdAt' => $t->getCreatedAt()?->getTimestamp(),
                'createdBy' => ['user'],
                'id' => null,
                'externalRef' => '',
                'currentPersons' => [],
                'currentAddressees' => [],
                'currentInputs' => [],
                'currentMotive' => null,
                'history' => [
                    [
                        'event_type' => 'create_ticket',
                    ],
                    [
                        'event_type' => 'state_change',
                    ],
                ],
                'currentState' => 'open',
                'updatedAt' => $t->getUpdatedAt()->getTimestamp(),
                'updatedBy' => ['user'],
                'emergency' => 'no',
                'caller' => null,
            ],
        ];

        // ticket with more features
        $ticket = new Ticket();

        // added by action
        new StateHistory(StateEnum::OPEN, $ticket, new \DateTimeImmutable('2024-06-16T00:00:00Z'));
        new EmergencyStatusHistory(EmergencyStatusEnum::YES, $ticket, new \DateTimeImmutable('2024-06-16T00:00:10Z'));

        // those are added by doctrine listeners
        $ticket->setCreatedAt(new \DateTimeImmutable('2024-06-16T00:00:00Z'));
        $ticket->setCreatedBy(new User());
        $ticket->setUpdatedAt(new \DateTimeImmutable('2024-06-16T00:00:00Z'));
        $ticket->setUpdatedBy(new User());
        $ticket->setExternalRef('2134');
        $personHistory = new PersonHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00'));
        $ticketHistory = new MotiveHistory(new Motive(), $ticket, new \DateTimeImmutable('2024-04-01T12:02:00'));
        $comment = new Comment('blabla test', $ticket);
        $comment->setCreatedAt(new \DateTimeImmutable('2024-04-01T12:04:00'));
        $comment->setCreatedBy(new User());
        $addresseeHistory = new AddresseeHistory(new User(), new \DateTimeImmutable('2024-04-01T12:05:00'), $ticket);
        $addresseeHistory->setEndDate(new \DateTimeImmutable('2024-04-01T12:06:00'));
        new AddresseeHistory(new UserGroup(), new \DateTimeImmutable('2024-04-01T12:07:00'), $ticket);

        yield [
            $ticket,
            [
                'type' => 'ticket_ticket',
                'type_extended' => 'ticket_ticket:extended',
                'createdAt' => $ticket->getCreatedAt()?->getTimestamp(),
                'createdBy' => ['user'],
                'id' => null,
                'externalRef' => '2134',
                'currentPersons' => ['embedded'],
                'currentAddressees' => ['embedded'],
                'currentInputs' => [],
                'currentMotive' => ['type' => 'motive', 'id' => 0],
                'history' => [
                    ['event_type' => 'add_person'],
                    ['event_type' => 'persons_state'],
                    ['event_type' => 'set_motive'],
                    ['event_type' => 'add_comment'],
                    ['event_type' => 'addressees_state'],
                    ['event_type' => 'addressees_state'],
                    ['event_type' => 'addressees_state'],
                    ['event_type' => 'create_ticket'],
                    ['event_type' => 'state_change'],
                    ['event_type' => 'emergency_change'],
                ],
                'currentState' => 'open',
                'updatedAt' => $ticket->getUpdatedAt()->getTimestamp(),
                'updatedBy' => ['user'],
                'emergency' => 'yes',
                'caller' => null,
            ],
        ];

        // ticket with caller
        $ticket = new Ticket();
        $ticket->setCreatedAt(new \DateTimeImmutable('2024-06-16T00:00:00'));
        $ticket->setUpdatedAt(new \DateTimeImmutable('2024-06-16T00:00:00'));
        new CallerHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00'));

        yield [
            $ticket,
            [
                'type' => 'ticket_ticket',
                'type_extended' => 'ticket_ticket:extended',
                'createdAt' => $ticket->getCreatedAt()?->getTimestamp(),
                'createdBy' => null,
                'id' => null,
                'externalRef' => '',
                'currentPersons' => [],
                'currentAddressees' => [],
                'currentInputs' => [],
                'currentMotive' => null,
                'history' => [
                    ['event_type' => 'set_caller'],
                ],
                'currentState' => 'open',
                'updatedAt' => $ticket->getUpdatedAt()->getTimestamp(),
                'updatedBy' => null,
                'emergency' => 'no',
                'caller' => ['person'],
            ],
        ];
    }

    public function testNormalizeTicketWithCommentNotAllowed(): void
    {
        $ticket = new Ticket();
        $comment = new Comment('Test comment', $ticket);
        $comment->setCreatedAt(new \DateTimeImmutable('2024-04-01T12:04:00'));
        $comment->setCreatedBy(new User());

        $this->security->isGranted(CommentVoter::READ, $comment)->willReturn(false)->shouldBeCalled();

        $actual = $this->buildNormalizer()->normalize($ticket, 'json', ['groups' => ['read']]);

        self::assertEmpty($actual['history']);
    }

    public function testNormalizeTicketWithCommentAllowed(): void
    {
        $ticket = new Ticket();
        $comment = new Comment('Test comment', $ticket);
        $comment->setCreatedAt(new \DateTimeImmutable('2024-04-01T12:04:00'));
        $comment->setCreatedBy(new User());

        $this->security->isGranted(CommentVoter::READ, $comment)->willReturn(true)->shouldBeCalled();

        $actual = $this->buildNormalizer()->normalize($ticket, 'json', ['groups' => ['read']]);

        self::assertCount(1, $actual['history']);
        self::assertEquals('add_comment', $actual['history'][0]['event_type']);
    }

    public function testNormalizeReadSimple(): void
    {
        // Create a ticket with some data
        $ticket = new Ticket();
        $ticket->setExternalRef('TEST-123');

        // Add state history
        new StateHistory(StateEnum::OPEN, $ticket, new \DateTimeImmutable('2024-06-16T00:00:00Z'));

        // Add emergency status
        new EmergencyStatusHistory(EmergencyStatusEnum::YES, $ticket, new \DateTimeImmutable('2024-06-16T00:00:10Z'));

        // Add person
        $personHistory = new PersonHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00'));

        // Add motive
        $motiveHistory = new MotiveHistory(new Motive(), $ticket, new \DateTimeImmutable('2024-04-01T12:02:00'));

        // Add comment
        $comment = new Comment('Test comment', $ticket);
        $comment->setCreatedAt(new \DateTimeImmutable('2024-04-01T12:04:00'));
        $comment->setCreatedBy(new User());

        // Add addressee
        new AddresseeHistory(new User(), new \DateTimeImmutable('2024-04-01T12:05:00'), $ticket);

        // Add caller
        new CallerHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00'));

        // Set created/updated metadata
        $ticket->setCreatedAt(new \DateTimeImmutable('2024-06-16T00:00:00Z'));
        $ticket->setCreatedBy(new User());
        $ticket->setUpdatedAt(new \DateTimeImmutable('2024-06-16T00:00:00Z'));
        $ticket->setUpdatedBy(new User());

        // Normalize with read:simple group
        $actual = $this->buildNormalizer()->normalize($ticket, 'json', ['groups' => 'read:simple']);

        // Expected keys in read:simple normalization
        $expectedKeys = [
            'type',
            'type_extended',
            'id',
            'externalRef',
            'currentPersons',
            'currentAddressees',
            'currentInputs',
            'currentMotive',
            'currentState',
            'emergency',
            'caller',
            'createdAt',
        ];

        // Keys that should not be present in read:simple normalization
        $unexpectedKeys = [
            'history',
            'updatedAt',
            'updatedBy',
            'createdBy',
        ];

        // Assert that all expected keys are present
        foreach ($expectedKeys as $key) {
            self::assertArrayHasKey($key, $actual, "Expected key '{$key}' is missing");
        }

        // Assert that none of the unexpected keys are present
        foreach ($unexpectedKeys as $key) {
            self::assertArrayNotHasKey($key, $actual, "Unexpected key '{$key}' is present");
        }

        // Assert specific values
        self::assertEquals('ticket_ticket', $actual['type']);
        self::assertEquals('ticket_ticket:simple', $actual['type_extended']);
        self::assertEquals('TEST-123', $actual['externalRef']);
        self::assertEquals('open', $actual['currentState']);
        self::assertEquals('yes', $actual['emergency']);
    }

    private function buildNormalizer(): TicketNormalizer
    {
        $this->security->isGranted(CommentVoter::READ, Argument::type(Comment::class))
            ->willReturn(true);


        $normalizer = $this->prophesize(NormalizerInterface::class);

        // empty array
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 0 === count($arg)),
            'json',
            Argument::type('array')
        )->willReturn([]);

        // array of mixed objects
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 0 < count($arg) && is_object($arg[0] ?? null)),
            'json',
            Argument::type('array')
        )->will(fn ($args) => array_fill(0, count($args[0]), 'embedded'));

        // array of event type
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 0 < count($arg) && is_array($arg[0] ?? null) && array_key_exists('event_type', $arg[0] ?? null)),
            'json',
            Argument::type('array')
        )->will(function ($args): array {
            $events = [];

            foreach ($args[0] as $event) {
                $events[] = $event['event_type'];
            }

            return $events;
        });
        // array of persons
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 1 === count($arg) && array_key_exists('persons', $arg)),
            'json',
            ['groups' => 'read']
        )->will(fn ($args): array => ['persons' => []]);
        // array of addresses
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 1 === count($arg) && array_key_exists('addressees', $arg)),
            'json',
            ['groups' => 'read']
        )->will(fn ($args): array => ['addressees' => []]);
        // state data
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 1 === count($arg) && array_key_exists('new_state', $arg)),
            'json',
            ['groups' => 'read']
        )->will(fn ($args): array => $args[0]);
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 1 === count($arg) && array_key_exists('new_emergency', $arg)),
            'json',
            ['groups' => 'read']
        )->will(fn ($args): array => $args[0]);
        $normalizer->normalize(
            Argument::that(fn ($arg) => is_array($arg) && 1 === count($arg) && array_key_exists('new_caller', $arg)),
            'json',
            ['groups' => 'read']
        )->will(fn ($args): array => ['new_caller' => ['dummy']]);

        // datetime
        $normalizer->normalize(Argument::type(\DateTimeImmutable::class), 'json', Argument::type('array'))
            ->will(fn ($args) => $args[0]->getTimestamp());
        // user
        $normalizer->normalize(Argument::type(User::class), 'json', Argument::type('array'))
            ->willReturn(['user']);
        // person
        $normalizer->normalize(Argument::type(Person::class), 'json', Argument::type('array'))
            ->willReturn(['person']);
        // motive
        $normalizer->normalize(Argument::type(Motive::class), 'json', Argument::type('array'))->willReturn(['type' => 'motive', 'id' => 0]);
        // person history
        $normalizer->normalize(Argument::type(PersonHistory::class), 'json', Argument::type('array'))
            ->willReturn(['personHistory']);
        // motive history
        $normalizer->normalize(Argument::type(MotiveHistory::class), 'json', Argument::type('array'))
            ->willReturn(['motiveHistory']);
        $normalizer->normalize(Argument::type(Comment::class), 'json', Argument::type('array'))
            ->willReturn(['comment']);
        $normalizer->normalize(Argument::type(AddresseeHistory::class), 'json', Argument::type('array'))
            ->willReturn(['addresseeHistory']);
        // null values
        $normalizer->normalize(null, 'json', Argument::type('array'))->willReturn(null);

        $ticketNormalizer = new TicketNormalizer($this->security->reveal());
        $ticketNormalizer->setNormalizer($normalizer->reveal());

        return $ticketNormalizer;
    }
}
