<?php

declare(strict_types=1);

namespace App\Tests\Functional\Mcp\Directory;

use App\Module\Core\Domain\Entity\User;
use App\Module\Directory\Domain\Entity\Client;
use App\Module\Directory\Domain\Entity\Prestataire;
use App\Module\Directory\Domain\Entity\Prospect;
use App\Module\Directory\Domain\Repository\AddressRepositoryInterface;
use App\Module\Directory\Domain\Repository\ClientRepositoryInterface;
use App\Module\Directory\Domain\Repository\PrestataireRepositoryInterface;
use App\Module\Directory\Domain\Repository\ProspectRepositoryInterface;
use App\Module\Directory\Infrastructure\Mcp\Tool\CreateAddressTool;
use App\Module\Directory\Infrastructure\Mcp\Tool\DeleteAddressTool;
use App\Module\Directory\Infrastructure\Mcp\Tool\GetAddressTool;
use App\Module\Directory\Infrastructure\Mcp\Tool\ListAddressesTool;
use App\Module\Directory\Infrastructure\Mcp\Tool\UpdateAddressTool;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\SecurityBundle\Security;

/**
 * @internal
 */
class AddressLifecycleTest extends KernelTestCase
{
    private EntityManagerInterface $em;
    private User $admin;
    private Client $client;
    private Prospect $prospect;
    private Prestataire $prestataire;

    protected function setUp(): void
    {
        self::bootKernel();
        $this->em = self::getContainer()->get(EntityManagerInterface::class);

        $this->admin = new User();
        $this->admin->setUsername('mcp-address-admin-'.uniqid());
        $this->admin->setPassword('x');
        $this->admin->setRoles(['ROLE_ADMIN']);
        $this->em->persist($this->admin);

        $this->client = new Client();
        $this->client->setName('Test Client '.uniqid());
        $this->em->persist($this->client);

        $this->prospect = new Prospect();
        $this->prospect->setCompany('Test Prospect '.uniqid());
        $this->em->persist($this->prospect);

        $this->prestataire = new Prestataire();
        $this->prestataire->setName('Test Prestataire '.uniqid());
        $this->em->persist($this->prestataire);

        $this->em->flush();
    }

    public function testCreateRequiresExactlyOneParent(): void
    {
        try {
            ($this->createTool())(null, null, null, 'Home');
            self::fail('Expected error when no parent provided.');
        } catch (InvalidArgumentException $e) {
            self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage());
        }

        try {
            ($this->createTool())($this->client->getId(), null, $this->prestataire->getId(), 'Dup');
            self::fail('Expected error when two parents provided.');
        } catch (InvalidArgumentException $e) {
            self::assertStringContainsString('Exactly one of clientId, prospectId or prestataireId', $e->getMessage());
        }
    }

    public function testCreateCountryDefaultsToFRWhenOmitted(): void
    {
        $data = json_decode(($this->createTool())($this->client->getId(), null, null, 'HQ'), true);

        self::assertSame('FR', $data['country']);
    }

    public function testCreateRejectsNonIso3166Country(): void
    {
        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('country must be a 2-letter ISO 3166 alpha-2 code');
        ($this->createTool())($this->client->getId(), null, null, 'HQ', null, null, null, null, 'France');
    }

    public function testCreateNormalizesCountryToUppercase(): void
    {
        $data = json_decode(($this->createTool())($this->client->getId(), null, null, 'HQ', null, null, null, null, 'be'), true);

        self::assertSame('BE', $data['country']);
    }

    public function testCreateOnEachParentWorks(): void
    {
        $clientAddr = json_decode(($this->createTool())($this->client->getId(), null, null, 'CHQ'), true);
        self::assertSame($this->client->getId(), $clientAddr['clientId']);
        self::assertNull($clientAddr['prospectId']);

        $prospectAddr = json_decode(($this->createTool())(null, $this->prospect->getId(), null, 'PHQ'), true);
        self::assertSame($this->prospect->getId(), $prospectAddr['prospectId']);

        $prestAddr = json_decode(($this->createTool())(null, null, $this->prestataire->getId(), 'XHQ'), true);
        self::assertSame($this->prestataire->getId(), $prestAddr['prestataireId']);
    }

    public function testGetReturnsAddress(): void
    {
        $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Office', '1 rue X', null, '75001', 'Paris', 'FR'), true);

        $data = json_decode(($this->getTool())((int) $created['id']), true);

        self::assertSame('Office', $data['label']);
        self::assertSame('1 rue X', $data['street']);
        self::assertSame('75001', $data['postalCode']);
        self::assertSame('Paris', $data['city']);
        self::assertSame('FR', $data['country']);
    }

    public function testListFilteredByClient(): void
    {
        ($this->createTool())($this->client->getId(), null, null, 'A');
        ($this->createTool())($this->client->getId(), null, null, 'B');
        ($this->createTool())(null, null, $this->prestataire->getId(), 'Z');

        $data = json_decode(($this->listTool())($this->client->getId(), null, null), true);

        self::assertCount(2, $data);
        self::assertSame('A', $data[0]['label']);
        self::assertSame('B', $data[1]['label']);
    }

    public function testUpdateRejectsNonIso3166Country(): void
    {
        $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'X'), true);

        $this->expectException(InvalidArgumentException::class);
        $this->expectExceptionMessage('country must be a 2-letter ISO 3166 alpha-2 code');
        ($this->updateTool())((int) $created['id'], null, null, null, null, null, 'Belgium');
    }

    public function testUpdateOnlyTouchesProvidedFields(): void
    {
        $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Old', '1 rue X', null, '75001', 'Paris', 'FR'), true);

        $data = json_decode(($this->updateTool())((int) $created['id'], 'New', null, null, '75002', null, 'be'), true);

        self::assertSame('New', $data['label']);     // changed
        self::assertSame('1 rue X', $data['street']); // unchanged
        self::assertSame('75002', $data['postalCode']); // changed
        self::assertSame('Paris', $data['city']);    // unchanged
        self::assertSame('BE', $data['country']);    // changed + uppercased
    }

    public function testDeleteRemovesAddress(): void
    {
        $created = json_decode(($this->createTool())($this->client->getId(), null, null, 'Bye'), true);
        $id      = (int) $created['id'];

        $data = json_decode(($this->deleteTool())($id), true);

        self::assertTrue($data['success']);

        $this->em->clear();
        self::assertNull(self::getContainer()->get(AddressRepositoryInterface::class)->findById($id));
    }

    private function securityFor(bool $admin = true): Security
    {
        $security = $this->createMock(Security::class);
        $security->method('isGranted')->willReturn($admin);
        $security->method('getUser')->willReturn($admin ? $this->admin : null);

        return $security;
    }

    private function createTool(): CreateAddressTool
    {
        $c = self::getContainer();

        return new CreateAddressTool(
            $this->em,
            $c->get(ClientRepositoryInterface::class),
            $c->get(ProspectRepositoryInterface::class),
            $c->get(PrestataireRepositoryInterface::class),
            $this->securityFor(),
        );
    }

    private function getTool(): GetAddressTool
    {
        return new GetAddressTool(
            self::getContainer()->get(AddressRepositoryInterface::class),
            $this->securityFor(),
        );
    }

    private function listTool(): ListAddressesTool
    {
        return new ListAddressesTool(
            self::getContainer()->get(AddressRepositoryInterface::class),
            $this->securityFor(),
        );
    }

    private function updateTool(): UpdateAddressTool
    {
        return new UpdateAddressTool(
            self::getContainer()->get(AddressRepositoryInterface::class),
            $this->em,
            $this->securityFor(),
        );
    }

    private function deleteTool(): DeleteAddressTool
    {
        return new DeleteAddressTool(
            self::getContainer()->get(AddressRepositoryInterface::class),
            $this->em,
            $this->securityFor(),
        );
    }
}
