<?php

declare(strict_types=1);

namespace Ferienpass\AdminBundle\Tests\Form;

use Ferienpass\AdminBundle\Form\FormScreen;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\FormConfigInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class FormScreenTest extends TestCase
{
    private function createFormChild(string $name, ?string $section): FormInterface
    {
        $config = $this->createStub(FormConfigInterface::class);
        $config->method('getOption')->willReturn($section);

        $child = $this->createStub(FormInterface::class);
        $child->method('getConfig')->willReturn($config);
        $child->method('getName')->willReturn($name);

        return $child;
    }

    private function createViolation(string $propertyPath, string $message = ''): ConstraintViolationInterface
    {
        $violation = $this->createStub(ConstraintViolationInterface::class);
        $violation->method('getPropertyPath')->willReturn($propertyPath);
        $violation->method('getMessage')->willReturn($message);

        return $violation;
    }

    private function createFormWithChildren(array $children): FormInterface
    {
        $form = $this->createStub(FormInterface::class);
        $form->method('all')->willReturn($children);

        $form->method('has')->willReturnCallback(function (string $name) use ($children): bool {
            foreach ($children as $child) {
                if ($child->getName() === $name) {
                    return true;
                }
            }

            return false;
        });

        $form->method('get')->willReturnCallback(function (string $name) use ($children): FormInterface {
            foreach ($children as $child) {
                if ($child->getName() === $name) {
                    return $child;
                }
            }
            throw new \OutOfBoundsException("Child \"$name\" does not exist.");
        });

        return $form;
    }

    // --- sections() ---

    public function testSectionsWithNoFieldsReturnsEmpty(): void
    {
        $form = $this->createFormWithChildren([]);
        $validator = $this->createStub(ValidatorInterface::class);

        $screen = new FormScreen($form, $validator, null);

        self::assertSame([], $screen->sections());
    }

    public function testSectionsExtractsUniqueSectionNames(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
            $this->createFormChild('description', 'general'),
            $this->createFormChild('date', 'schedule'),
        ];
        $form = $this->createFormWithChildren($children);
        $validator = $this->createStub(ValidatorInterface::class);

        $screen = new FormScreen($form, $validator, null);

        self::assertSame(['general', 'schedule'], $screen->sections());
    }

    public function testSectionsMergesExtraSections(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
        ];
        $form = $this->createFormWithChildren($children);
        $validator = $this->createStub(ValidatorInterface::class);

        $screen = (new FormScreen($form, $validator, null))
            ->withExtraSections(['media', 'hosts']);

        self::assertSame(['general', 'media', 'hosts'], $screen->sections());
    }

    public function testSectionsRespectsSectionOrder(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
            $this->createFormChild('date', 'schedule'),
        ];
        $form = $this->createFormWithChildren($children);
        $validator = $this->createStub(ValidatorInterface::class);

        $screen = (new FormScreen($form, $validator, null))
            ->withExtraSections(['media', 'hosts'])
            ->withSectionOrder(['schedule', 'media', 'general']);

        self::assertSame(['schedule', 'media', 'general', 'hosts'], $screen->sections());
    }

    public function testSectionsFiltersOutNullSections(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
            $this->createFormChild('hidden', null),
            $this->createFormChild('date', 'schedule'),
        ];
        $form = $this->createFormWithChildren($children);
        $validator = $this->createStub(ValidatorInterface::class);

        $screen = new FormScreen($form, $validator, null);

        self::assertSame(['general', 'schedule'], $screen->sections());
    }

    // --- violatedProperties() ---

    public function testViolatedPropertiesCallsValidatorWithWarningAndErrorGroups(): void
    {
        $form = $this->createFormWithChildren([]);
        $formData = new \stdClass();
        $validator = $this->createMock(ValidatorInterface::class);

        $warningViolation = $this->createViolation('title');
        $errorViolation = $this->createViolation('date');

        $validator->expects(self::exactly(2))
            ->method('validate')
            ->willReturnCallback(function (mixed $value, mixed $constraints, mixed $groups) use ($warningViolation, $errorViolation) {
                if ('warning' === $groups) {
                    return new ConstraintViolationList([$warningViolation]);
                }
                if ('error' === $groups) {
                    return new ConstraintViolationList([$errorViolation]);
                }

                return new ConstraintViolationList();
            });

        $screen = new FormScreen($form, $validator, $formData);
        $result = $screen->violatedProperties();

        self::assertSame(['title'], $result['warning']);
        self::assertSame(['date'], $result['error']);
    }

    public function testViolatedPropertiesCachesResults(): void
    {
        $form = $this->createFormWithChildren([]);
        $formData = new \stdClass();
        $validator = $this->createMock(ValidatorInterface::class);

        $validator->expects(self::exactly(2))
            ->method('validate')
            ->willReturn(new ConstraintViolationList());

        $screen = new FormScreen($form, $validator, $formData);
        $screen->violatedProperties();
        $screen->violatedProperties();

        // exactly(2) ensures validate is only called twice (once per group), not four times
    }

    // --- violatedSections() ---

    public function testViolatedSectionsMapsPropertiesToTheirFormSection(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
            $this->createFormChild('date', 'schedule'),
        ];
        $form = $this->createFormWithChildren($children);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturnCallback(function (mixed $value, mixed $constraints, mixed $groups) {
                if ('warning' === $groups) {
                    return new ConstraintViolationList([$this->createViolation('title')]);
                }
                if ('error' === $groups) {
                    return new ConstraintViolationList([$this->createViolation('date')]);
                }

                return new ConstraintViolationList();
            });

        $screen = new FormScreen($form, $validator, $formData);
        $result = $screen->violatedSections();

        self::assertContains('general', $result['warning']);
        self::assertContains('schedule', $result['error']);
    }

    // --- warningMessages() ---

    public function testWarningMessagesReturnsMessagesForSpecificSection(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
            $this->createFormChild('date', 'schedule'),
        ];
        $form = $this->createFormWithChildren($children);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturnCallback(function (mixed $value, mixed $constraints, mixed $groups) {
                if ('warning' === $groups) {
                    return new ConstraintViolationList([
                        $this->createViolation('title', 'Title is recommended'),
                        $this->createViolation('date', 'Date should be set'),
                    ]);
                }

                return new ConstraintViolationList();
            });

        $screen = new FormScreen($form, $validator, $formData);

        self::assertSame(['Title is recommended'], $screen->warningMessages('general'));
        self::assertSame(['Date should be set'], $screen->warningMessages('schedule'));
    }

    public function testWarningMessagesDeduplicatesIdenticalMessages(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
            $this->createFormChild('subtitle', 'general'),
        ];
        $form = $this->createFormWithChildren($children);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturnCallback(function (mixed $value, mixed $constraints, mixed $groups) {
                if ('warning' === $groups) {
                    return new ConstraintViolationList([
                        $this->createViolation('title', 'Field is recommended'),
                        $this->createViolation('subtitle', 'Field is recommended'),
                    ]);
                }

                return new ConstraintViolationList();
            });

        $screen = new FormScreen($form, $validator, $formData);

        self::assertSame(['Field is recommended'], $screen->warningMessages('general'));
    }

    public function testWarningMessagesReturnsEmptyForSectionWithNoWarnings(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
        ];
        $form = $this->createFormWithChildren($children);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturn(new ConstraintViolationList());

        $screen = new FormScreen($form, $validator, $formData);

        self::assertSame([], $screen->warningMessages('general'));
    }

    // --- hasWarning() / hasError() ---

    public function testHasWarningReturnsTrueWhenSectionHasWarning(): void
    {
        $children = [
            $this->createFormChild('title', 'general'),
        ];
        $form = $this->createFormWithChildren($children);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturnCallback(function (mixed $value, mixed $constraints, mixed $groups) {
                if ('warning' === $groups) {
                    return new ConstraintViolationList([$this->createViolation('title')]);
                }

                return new ConstraintViolationList();
            });

        $screen = new FormScreen($form, $validator, $formData);

        self::assertTrue($screen->hasWarning('general'));
    }

    public function testHasWarningReturnsFalseWhenSectionHasNoWarning(): void
    {
        $form = $this->createFormWithChildren([]);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturn(new ConstraintViolationList());

        $screen = new FormScreen($form, $validator, $formData);

        self::assertFalse($screen->hasWarning('general'));
    }

    public function testHasErrorReturnsTrueWhenSectionHasError(): void
    {
        $children = [
            $this->createFormChild('date', 'schedule'),
        ];
        $form = $this->createFormWithChildren($children);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturnCallback(function (mixed $value, mixed $constraints, mixed $groups) {
                if ('error' === $groups) {
                    return new ConstraintViolationList([$this->createViolation('date')]);
                }

                return new ConstraintViolationList();
            });

        $screen = new FormScreen($form, $validator, $formData);

        self::assertTrue($screen->hasError('schedule'));
    }

    public function testHasErrorReturnsFalseWhenSectionHasNoError(): void
    {
        $form = $this->createFormWithChildren([]);
        $formData = new \stdClass();
        $validator = $this->createStub(ValidatorInterface::class);

        $validator->method('validate')
            ->willReturn(new ConstraintViolationList());

        $screen = new FormScreen($form, $validator, $formData);

        self::assertFalse($screen->hasError('schedule'));
    }

    // --- sectionLabel() / sectionDescription() ---

    public function testSectionLabelReplacesLabelWithLegendAndNameWithSection0(): void
    {
        $config = $this->createStub(FormConfigInterface::class);
        $config->method('getOption')->willReturn('label.offer.%name%');

        $form = $this->createStub(FormInterface::class);
        $form->method('getConfig')->willReturn($config);

        $validator = $this->createStub(ValidatorInterface::class);

        $screen = new FormScreen($form, $validator, null);
        $result = $screen->sectionLabel('general');

        self::assertInstanceOf(TranslatableMessage::class, $result);
        self::assertSame('legend.offer.general.0', (string) $result);
    }

    public function testSectionDescriptionReplacesLabelWithLegendAndNameWithSection1(): void
    {
        $config = $this->createStub(FormConfigInterface::class);
        $config->method('getOption')->willReturn('label.offer.%name%');

        $form = $this->createStub(FormInterface::class);
        $form->method('getConfig')->willReturn($config);

        $validator = $this->createStub(ValidatorInterface::class);

        $screen = new FormScreen($form, $validator, null);
        $result = $screen->sectionDescription('general');

        self::assertInstanceOf(TranslatableMessage::class, $result);
        self::assertSame('legend.offer.general.1', (string) $result);
    }
}
