<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\Tests\Filter;

use Doctrine\ORM\QueryBuilder;
use Ferienpass\CmsBundle\Filter\Type\Offer\AgeType;
use Ferienpass\CmsBundle\Filter\Type\Offer\CategoryType;
use Ferienpass\CmsBundle\Filter\Type\Offer\EarliestDateType;
use Ferienpass\CmsBundle\Filter\Type\Offer\FeeType;
use Ferienpass\CmsBundle\Filter\Type\Offer\FreeSeatsType;
use Ferienpass\CmsBundle\Filter\Type\Offer\FreeType;
use Ferienpass\CmsBundle\Filter\Type\Offer\LatestDateType;
use Ferienpass\CmsBundle\Filter\Type\Offer\NameType;
use Ferienpass\CmsBundle\Filter\Type\Offer\NoApplicationType;
use Ferienpass\CmsBundle\Filter\Type\Offer\WheelchairAccessibleType;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Translation\TranslatableMessage;

final class OfferFilterTypesTest extends TestCase
{
    // ---- NameType ----

    public function testNameTypeGetName(): void
    {
        self::assertSame('titel', NameType::getName());
    }

    public function testNameTypeGetParent(): void
    {
        self::assertSame(SearchType::class, new NameType()->getParent());
    }

    public function testNameTypeGetHumanReadableValueReturnsTranslatableMessage(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getViewData')->willReturn('test');

        $result = new NameType()->getHumanReadableValue($form);

        self::assertInstanceOf(TranslatableMessage::class, $result);
    }

    public function testNameTypeApplyDoesNothingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new NameType()->apply($qb, $form);
    }

    // ---- FreeType ----

    public function testFreeTypeGetName(): void
    {
        self::assertSame('kostenlos', FreeType::getName());
    }

    public function testFreeTypeGetParent(): void
    {
        self::assertSame(CheckboxType::class, new FreeType()->getParent());
    }

    public function testFreeTypeApplyDoesNothingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new FreeType()->apply($qb, $form);
    }

    // ---- WheelchairAccessibleType ----

    public function testWheelchairAccessibleTypeGetName(): void
    {
        self::assertSame('rollstuhl', WheelchairAccessibleType::getName());
    }

    // ---- AgeType (multi-value: numeric ages and bands like "3-6", "16+") ----

    public function testAgeTypeGetName(): void
    {
        self::assertSame('alter', AgeType::getName());
    }

    public function testAgeTypeGetParent(): void
    {
        // FormType + compound=false so the field can hold an array as-is.
        self::assertSame(FormType::class, new AgeType()->getParent());
    }

    public function testAgeTypeApplyDoesNothingWhenDataIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn([]);
        $form->method('getName')->willReturn('alter');

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new AgeType()->apply($qb, $form);
    }

    public function testAgeTypeApplyDoesNothingWhenDataIsNull(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(null);
        $form->method('getName')->willReturn('alter');

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new AgeType()->apply($qb, $form);
    }

    public function testAgeTypeApplyParsesSingleNumericAge(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(['7']);
        $form->method('getName')->willReturn('alter');

        $params = [];
        $expr = $this->createMock(\Doctrine\ORM\Query\Expr::class);
        $orx = new \Doctrine\ORM\Query\Expr\Orx();
        $expr->method('orX')->willReturnCallback(static fn (...$args) => new \Doctrine\ORM\Query\Expr\Orx($args));

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects(self::once())->method('andWhere');
        $qb->method('setParameter')->willReturnCallback(static function ($k, $v) use (&$params, $qb) {
            $params[$k] = $v;

            return $qb;
        });

        new AgeType()->apply($qb, $form);

        // For age "7" we should clamp both low and high to 7.
        self::assertSame(7, $params['q_alter_l_0']);
        self::assertSame(7, $params['q_alter_h_0']);
    }

    public function testAgeTypeApplyParsesBandValue(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(['3-6']);
        $form->method('getName')->willReturn('alter');

        $params = [];
        $expr = $this->createMock(\Doctrine\ORM\Query\Expr::class);
        $expr->method('orX')->willReturnCallback(static fn (...$args) => new \Doctrine\ORM\Query\Expr\Orx($args));

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects(self::once())->method('andWhere');
        $qb->method('setParameter')->willReturnCallback(static function ($k, $v) use (&$params, $qb) {
            $params[$k] = $v;

            return $qb;
        });

        new AgeType()->apply($qb, $form);

        self::assertSame(3, $params['q_alter_l_0']);
        self::assertSame(6, $params['q_alter_h_0']);
    }

    public function testAgeTypeApplyParsesOpenEndedBand(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(['16+']);
        $form->method('getName')->willReturn('alter');

        $params = [];
        $expr = $this->createMock(\Doctrine\ORM\Query\Expr::class);
        $expr->method('orX')->willReturnCallback(static fn (...$args) => new \Doctrine\ORM\Query\Expr\Orx($args));

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects(self::once())->method('andWhere');
        $qb->method('setParameter')->willReturnCallback(static function ($k, $v) use (&$params, $qb) {
            $params[$k] = $v;

            return $qb;
        });

        new AgeType()->apply($qb, $form);

        self::assertSame(16, $params['q_alter_l_0']);
        self::assertSame(99, $params['q_alter_h_0']);
    }

    public function testAgeTypeApplyMultipleValuesAreOredWithDistinctParams(): void
    {
        $form = $this->createMock(FormInterface::class);
        // Two siblings: 7 and 14.
        $form->method('getData')->willReturn(['7', '14']);
        $form->method('getName')->willReturn('alter');

        $params = [];
        $expr = $this->createMock(\Doctrine\ORM\Query\Expr::class);
        $expr->method('orX')->willReturnCallback(static fn (...$args) => new \Doctrine\ORM\Query\Expr\Orx($args));

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects(self::once())->method('andWhere');
        $qb->method('setParameter')->willReturnCallback(static function ($k, $v) use (&$params, $qb) {
            $params[$k] = $v;

            return $qb;
        });

        new AgeType()->apply($qb, $form);

        self::assertSame(7, $params['q_alter_l_0']);
        self::assertSame(7, $params['q_alter_h_0']);
        self::assertSame(14, $params['q_alter_l_1']);
        self::assertSame(14, $params['q_alter_h_1']);
    }

    public function testAgeTypeApplySkipsGibberish(): void
    {
        $form = $this->createMock(FormInterface::class);
        // Mix valid + invalid; only the valid one should produce a clause.
        $form->method('getData')->willReturn(['abc', '12']);
        $form->method('getName')->willReturn('alter');

        $params = [];
        $expr = $this->createMock(\Doctrine\ORM\Query\Expr::class);
        $expr->method('orX')->willReturnCallback(static fn (...$args) => new \Doctrine\ORM\Query\Expr\Orx($args));

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects(self::once())->method('andWhere');
        $qb->method('setParameter')->willReturnCallback(static function ($k, $v) use (&$params, $qb) {
            $params[$k] = $v;

            return $qb;
        });

        new AgeType()->apply($qb, $form);

        // Only one valid value → only index 0 set.
        self::assertSame(12, $params['q_alter_l_0']);
        self::assertSame(12, $params['q_alter_h_0']);
        self::assertArrayNotHasKey('q_alter_l_1', $params);
    }

    public function testAgeTypeApplyAcceptsScalarStringForBackwardsCompat(): void
    {
        // Old `?alter=7` should still work — normalize() promotes the
        // scalar to a single-element array.
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn('7');
        $form->method('getName')->willReturn('alter');

        $params = [];
        $expr = $this->createMock(\Doctrine\ORM\Query\Expr::class);
        $expr->method('orX')->willReturnCallback(static fn (...$args) => new \Doctrine\ORM\Query\Expr\Orx($args));

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects(self::once())->method('andWhere');
        $qb->method('setParameter')->willReturnCallback(static function ($k, $v) use (&$params, $qb) {
            $params[$k] = $v;

            return $qb;
        });

        new AgeType()->apply($qb, $form);

        self::assertSame(7, $params['q_alter_l_0']);
        self::assertSame(7, $params['q_alter_h_0']);
    }

    public function testAgeTypeGetHumanReadableValueReturnsNullWhenEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn([]);

        self::assertNull(new AgeType()->getHumanReadableValue($form));
    }

    public function testAgeTypeGetHumanReadableValueReturnsTranslatableMessage(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(['7', '14']);

        self::assertInstanceOf(TranslatableMessage::class, new AgeType()->getHumanReadableValue($form));
    }

    // ---- EarliestDateType ----

    public function testEarliestDateTypeGetName(): void
    {
        self::assertSame('von', EarliestDateType::getName());
    }

    public function testEarliestDateTypeGetParent(): void
    {
        self::assertSame(DateType::class, new EarliestDateType()->getParent());
    }

    public function testEarliestDateTypeApplyCallsAndHavingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $expr = $this->createMock(\Doctrine\ORM\Query\Expr::class);
        $expr->method('orX')->willReturn(new \Doctrine\ORM\Query\Expr\Orx());

        $qb = $this->createMock(QueryBuilder::class);
        $qb->method('expr')->willReturn($expr);
        $qb->expects(self::once())->method('andHaving');

        new EarliestDateType()->apply($qb, $form);
    }

    public function testEarliestDateTypeGetHumanReadableValueReturnsNullForNonDate(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(null);

        self::assertNull(new EarliestDateType()->getHumanReadableValue($form));
    }

    public function testEarliestDateTypeGetHumanReadableValueReturnsTranslatableMessage(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(new \DateTime('2024-07-01'));

        self::assertInstanceOf(TranslatableMessage::class, new EarliestDateType()->getHumanReadableValue($form));
    }

    // ---- LatestDateType ----

    public function testLatestDateTypeGetName(): void
    {
        self::assertSame('bis', LatestDateType::getName());
    }

    public function testLatestDateTypeGetParent(): void
    {
        self::assertSame(DateType::class, new LatestDateType()->getParent());
    }

    public function testLatestDateTypeApplyDoesNothingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andHaving');

        new LatestDateType()->apply($qb, $form);
    }

    public function testLatestDateTypeGetHumanReadableValueReturnsNullForNonDate(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(null);

        self::assertNull(new LatestDateType()->getHumanReadableValue($form));
    }

    public function testLatestDateTypeGetHumanReadableValueReturnsTranslatableMessage(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getData')->willReturn(new \DateTime('2024-08-31'));

        self::assertInstanceOf(TranslatableMessage::class, new LatestDateType()->getHumanReadableValue($form));
    }

    // ---- FeeType ----

    public function testFeeTypeGetName(): void
    {
        self::assertSame('gebuehr', FeeType::getName());
    }

    public function testFeeTypeGetParent(): void
    {
        self::assertSame(MoneyType::class, new FeeType()->getParent());
    }

    public function testFeeTypeApplyDoesNothingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new FeeType()->apply($qb, $form);
    }

    public function testFeeTypeGetHumanReadableValueReturnsTranslatableMessage(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('getViewData')->willReturn('500');

        self::assertInstanceOf(TranslatableMessage::class, new FeeType()->getHumanReadableValue($form));
    }

    // ---- NoApplicationType ----

    public function testNoApplicationTypeGetName(): void
    {
        self::assertSame('ohne_anmeldung', NoApplicationType::getName());
    }

    public function testNoApplicationTypeGetParent(): void
    {
        self::assertSame(CheckboxType::class, new NoApplicationType()->getParent());
    }

    public function testNoApplicationTypeApplyDoesNothingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new NoApplicationType()->apply($qb, $form);
    }

    public function testNoApplicationTypeGetHumanReadableValueReturnsTranslatableMessage(): void
    {
        $form = $this->createMock(FormInterface::class);

        self::assertInstanceOf(TranslatableMessage::class, new NoApplicationType()->getHumanReadableValue($form));
    }

    // ---- FreeSeatsType ----

    public function testFreeSeatsTypeGetName(): void
    {
        self::assertSame('sofortbuchung', FreeSeatsType::getName());
    }

    public function testFreeSeatsTypeGetParent(): void
    {
        self::assertSame(CheckboxType::class, new FreeSeatsType()->getParent());
    }

    public function testFreeSeatsTypeApplyDoesNothingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new FreeSeatsType()->apply($qb, $form);
    }

    public function testFreeSeatsTypeGetHumanReadableValueReturnsTranslatableMessage(): void
    {
        $form = $this->createMock(FormInterface::class);

        self::assertInstanceOf(TranslatableMessage::class, new FreeSeatsType()->getHumanReadableValue($form));
    }

    // ---- CategoryType ----

    public function testCategoryTypeGetName(): void
    {
        self::assertSame('kategorie', CategoryType::getName());
    }

    public function testCategoryTypeGetParent(): void
    {
        self::assertSame(EntityType::class, new CategoryType()->getParent());
    }

    public function testCategoryTypeApplyDoesNothingWhenFormIsEmpty(): void
    {
        $form = $this->createMock(FormInterface::class);
        $form->method('isEmpty')->willReturn(true);

        $qb = $this->createMock(QueryBuilder::class);
        $qb->expects(self::never())->method('andWhere');

        new CategoryType()->apply($qb, $form);
    }
}
