<?php

/**
 * Questo file è parte di Pongho.
 *
 * @author    Daniele Termini
 * @copyright Web Agency Meta Line S.r.l.
 * @package   Application\Admin
 */

namespace Application\Admin\Tests\Form;

use Application\Admin\Form\FormConfig;
use Application\Admin\Form\Generator\AdminGenerator;
use Pongho\Core\Tests\Mock\Localization;
use Pongho\Form\Field\CheckboxField;
use Pongho\Form\Field\ChecklistField;
use Pongho\Form\Field\HiddenField;
use Pongho\Form\Field\PasswordField;
use Pongho\Form\Field\RadioField;
use Pongho\Form\Field\SelectField;
use Pongho\Form\Field\TextareaField;
use Pongho\Form\Field\TextField;
use Pongho\Form\Subject\ArrayRowSubject;
use Pongho\Form\Utilities\FormBuilder;
use Symfony\Component\DomCrawler\Crawler;

class AdminGeneratorTest extends \PHPUnit\Framework\TestCase
{
    /** @var \Application\Admin\Form\Generator\AdminGenerator */
    protected $generator;

    protected function setUp(): void
    {
        $this->generator = new AdminGenerator(new Localization());
    }

    /**
     * Configuro un SelectField e testo le varie parti del template renderizzato
     */
    public function testSelectField(): void
    {
        $select = new SelectField('test');
        $select->setGenerator($this->generator);

        $select->setOptions([
            'a_value' => 'A',
            'B'       => [
                'optgroup' => [
                    'b_value' => 'B',
                    'c_value' => 'C',
                ],
            ],
            'd_value' => ['D'], // Formato per la select gerarchica
        ]);

        /**
         * Struttura
         */
        $this->assertTagTwitterBootstrap($select->render());
        $this->assertSelectStructure($select->render());

        /**
         * Valore selezionato
         */
        $values = ['a_value' => 'A', 'b_value' => 'B', 'c_value' => 'C', 'd_value' => 'D'];

        $select->setValue('a_value');
        $this->assertSelectHasSelectedValues($select->render(), ['a_value' => 'A'], $values);

        // Cambio il valore
        $select->setValue('b_value');
        $this->assertSelectHasSelectedValues($select->render(), ['b_value' => 'B'], $values);

        /**
         * Struttura - Sola lettura
         */
        $select->setReadonly();

        /**
         * Controllo che il precedente valore selezionato sia rimasto visualizzato ma in sola lettura
         */
        $this->assertSelectHasSelectedValues($select->render(), ['b_value' => 'B'], $values, true);

        // Cambio il valore
        $select->setValue('d_value');
        $this->assertSelectHasSelectedValues($select->render(), ['d_value' => 'D'], $values, true);

        /**
         * Select multipla - sola lettura (precedentemente impostato)
         */
        $select->setAttribute('multiple', 'multiple');
        $select->setValue(['a_value', 'b_value', 'd_value']);
        $this->assertSelectHasSelectedValues($select->render(), ['a_value' => 'A', 'b_value' => 'B', 'd_value' => 'D'], $values, true);

        // Cambio il valore
        $select->setValue(['b_value', 'c_value']);
        $this->assertSelectHasSelectedValues($select->render(), ['b_value' => 'B', 'c_value' => 'C'], $values, true);

        /**
         * Select multipla (rimuovo la flag sola lettura precedentemente impostata)
         */
        $select->setReadonly(false);
        $this->assertSelectHasSelectedValues($select->render(), ['b_value' => 'B', 'c_value' => 'C'], $values);

        // Cambio il valore
        $select->setValue(['a_value', 'd_value']);
        $this->assertSelectHasSelectedValues($select->render(), ['a_value' => 'A', 'd_value' => 'D'], $values);
    }

    /**
     * Testo che il campo sia involucrato nel Twitter Bootstrap
     *
     * @param $html
     */
    protected function assertTagTwitterBootstrap($html)
    {
        $crawler = new Crawler($html);

        $label = $crawler->filter('div.control-group label.control-label');
        $this->assertEquals(1, $label->count(), $html);
        $this->assertEquals('field-test', $label->attr('for'));
    }

    /**
     * Struttura del campo SelectField, compreso le opzioni impostate
     *
     * @param $html
     */
    protected function assertSelectStructure($html)
    {
        $crawler = new Crawler($html);

        $select = $crawler->filter('div .controls select#field-test');
        $this->assertEquals(1, $select->count());
        $this->assertEquals('test', $select->attr('name'));

        /**
         * Opzioni
         */
        $this->assertEquals(4, $select->filter('option')->count());

        $optgroup = $select->filter('optgroup');
        $this->assertEquals(1, $optgroup->count());
        $this->assertEquals(2, $optgroup->filter('option')->count(), $html);
    }

    /**
     * @param         $is_readonly
     * @param         $is_multiple
     * @return Crawler
     */
    protected function getSelectNode(Crawler $crawler, $is_readonly, $is_multiple)
    {
        $tag = 'select';

        if ($is_readonly) {
            $tag = 'span';

            if ($is_multiple) {
                $tag = 'ul';
            }
        }

        return $select = $crawler->filter('div.controls ' . $tag . '#field-test');
    }

    /**
     * Metodo specifico per controllare che sia selezionata una determinata opzione
     *
     * @param       $html
     * @param bool  $readonly
     * @throws \InvalidArgumentException
     */
    protected function assertSelectHasSelectedValues($html, array $selected_value, array $values, $readonly = false)
    {
        if ($values === []) {
            throw new \InvalidArgumentException('Non è possibile testare le opzioni di una select senza specificare i valori');
        }

        if (array_intersect_assoc($selected_value, $values) !== $selected_value) {
            throw new \InvalidArgumentException('Le opzioni selezionate non sono presenti nel array dei valori, controlla il test!');
        }

        $multiple = count($selected_value) > 1;

        $crawler = new Crawler($html);
        $select = $this->getSelectNode($crawler, $readonly, $multiple);
        $this->assertEquals(1, $select->count());

        if ($multiple && $readonly) {
            $this->assertEquals(count($selected_value), $select->filter('li')->count());
        } elseif ($readonly) {
            $this->assertEquals(count($selected_value), $select->filter('span')->count());
        } else {
            $this->assertEquals(count($selected_value), $select->filter('option:selected')->count());
        }
    }

    /**
     * Testo il campo Text
     */
    public function testTextField(): void
    {
        $field = new TextField('test');
        $field->setGenerator($this->generator);

        $this->assertTagTwitterBootstrap($field->render());

        /**
         * Struttura base del campo
         */
        $crawler = new Crawler($field->render());
        $text = $crawler->filter('input.input_text[type="text"]');
        $this->assertEquals(1, $text->count());

        /**
         * Valori visualizzati nei vari stati del campo
         */
        $this->assertTextValue($field, 'hello, this is test');
        $this->assertTextValue($field, 'hello, this is test', true);

        // Cambio il valore
        $this->assertTextValue($field, 'hello test, this is value');
        $this->assertTextValue($field, 'hello test, this is value', true);
    }

    /**
     * Controllo il valore del campo Text
     *
     * @param           $value
     * @param bool      $readonly
     */
    protected function assertTextValue(TextField $field, $value, $readonly = false)
    {
        $field->setReadonly($readonly);
        $field->setValue($value);

        $crawler = new Crawler($field->render());

        if ($readonly) {
            $text = $crawler->filter('span#field-test');
            $this->assertEquals(1, $text->count());
            $this->assertEquals($value, $text->html());
        } else {
            $text = $crawler->filter('input#field-test');
            $this->assertEquals(1, $text->count());
            $this->assertEquals($value, $text->attr('value'));
        }
    }

    /**
     * Testo il campo Textarea
     */
    public function testTextareaField(): void
    {
        $field = new TextareaField('test');
        $field->setGenerator($this->generator);

        $this->assertTagTwitterBootstrap($field->render());

        /**
         * Struttura base del campo
         */
        $crawler = new Crawler($field->render());
        $this->assertEquals(1, $crawler->filter('textarea')->count());

        /**
         * Valori visualizzati nei vari stati del campo
         */
        $value = <<<HTML
hello -!/#,
this is test
HTML;
        $this->assertTextareaContent($field, $value);
        $this->assertTextareaContent($field, $value, true);

        // Cambio il valore
        $value = <<<HTML
<p>hello test, this is value</p>
HTML;
        $this->assertTextareaContent($field, $value);
        $this->assertTextareaContent($field, $value, true);
    }

    /**
     * Testo il contenuto della textarea
     *
     * @param               $content
     * @param bool          $readonly
     */
    protected function assertTextareaContent(TextareaField $field, $content, $readonly = false)
    {
        $field->setValue($content);
        $field->setReadonly($readonly);

        $crawler = new Crawler($field->render());

        if ($readonly) {
            $this->assertEquals(1, $crawler->filter('div.controls div#field-test')->count());
            $this->assertEquals(nl2br(html_escape($content), false), $crawler->filter('div.controls div#field-test')->html(), $content);
        } else {
            $this->assertEquals(1, $crawler->filter('div.controls textarea#field-test')->count());
            $this->assertEquals(html_escape($content), $crawler->filter('div.controls textarea#field-test')->html(), $content);
        }
    }

    /**
     * Testo il campo Checkbox
     */
    public function testCheckboxField(): void
    {
        $field = new CheckboxField('test');
        $field->setGenerator($this->generator);

        $this->assertTagTwitterBootstrap($field->render());

        /**
         * Struttura base del campo
         */
        $crawler = new Crawler($field->render());

        $checkbox = $this->getCheckboxNode($crawler);
        $this->assertEquals(1, $checkbox->count());

        /**
         * Valori visualizzati nei vari stati del campo
         */
        $this->assertCheckboxValue($field, true);
        $this->assertCheckboxValue($field, false);

        $this->assertCheckboxValue($field, true, true);
        $this->assertCheckboxValue($field, false, true);
    }

    /**
     * @param         $is_readonly
     * @return Crawler
     */
    protected function getCheckboxNode(Crawler $crawler, $is_readonly = false)
    {
        if ($is_readonly) {
            return $crawler->filter('span#field-test');
        } else {
            return $crawler->filter('input#field-test[type="checkbox"]');
        }
    }

    /**
     * @param               $value
     * @param bool          $readonly
     */
    protected function assertCheckboxValue(CheckboxField $field, $value, $readonly = false)
    {
        $field->setReadonly($readonly);
        $field->setValue($value);

        $crawler = new Crawler($field->render());
        $checkbox = $this->getCheckboxNode($crawler, $readonly);

        if ($readonly) {
            if ($value) {
                $this->assertEquals('yes', $checkbox->html());
            } else {
                $this->assertEquals('no', $checkbox->html());
            }
        } elseif ($value) {
            $this->assertEquals('checked', $checkbox->attr('checked'));
        } else {
            $this->assertEquals('', $checkbox->attr('checked'));
        }
    }


    /**
     * Testo il campo Checklist
     */
    public function testChecklistField(): void
    {
        $values = ['a_value' => 'A', 'b_value' => 'B', 'c_value' => 'C', 'd_value' => 'D'];
        $selected_values = ['b_value' => 'B', 'c_value' => 'C'];

        $field = new ChecklistField('test');
        $field->setGenerator($this->generator);
        $field->setOptions($values);
        $field->setValue(array_keys($selected_values));

        $this->assertTagTwitterBootstrap($field->render());
        $this->assertChecklistStructure($field->render(), count($values));

        $this->assertChecklistHasSelectedValues($field->render(), $selected_values, $values);

        $field->setReadonly();

        $this->assertChecklistStructureReadonly($field->render(), count($selected_values));
        $this->assertChecklistHasSelectedValues($field->render(), $selected_values, $values, true);
    }

    /**
     * @param $html
     * @param $count
     */
    protected function assertChecklistStructure($html, $count)
    {
        $crawler = new Crawler($html);

        $this->assertEquals(1, $crawler->filter('div.controls')->count());
        $this->assertEquals($count, $crawler->filter('label.field-option')->count());
    }

    /**
     * @param $html
     * @param $count
     */
    protected function assertChecklistStructureReadonly($html, $count)
    {
        $crawler = new Crawler($html);

        $this->assertEquals($count, $crawler->filter('.controls ul#field-test li')->count());
    }

    /**
     * @param       $html
     * @param bool  $readonly
     * @throws \InvalidArgumentException
     */
    protected function assertChecklistHasSelectedValues($html, array $selected_value, array $values, $readonly = false)
    {
        if ($values === []) {
            throw new \InvalidArgumentException('Non è possibile testare le opzioni di una checklist senza specificare i valori');
        }

        if (array_intersect_assoc($selected_value, $values) !== $selected_value) {
            throw new \InvalidArgumentException('Le opzioni selezionate non sono presente nel array dei valori, controlla il test!');
        }

        $crawler = new Crawler($html);

        $selector = $readonly ? 'li' : 'input[type="checkbox"]:checked';
        $count = $crawler->filter($selector)->count();

        $this->assertEquals(count($selected_value), $count);
    }

    /**
     * Testo il campo Password
     */
    public function testPasswordField(): void
    {
        $field = new PasswordField('test');
        $field->setGenerator($this->generator);
        $field->setValue('password');

        $this->assertTagTwitterBootstrap($field->render());

        /**
         * Struttura di base del campo
         */
        $crawler = new Crawler($field->render());
        $tag = $crawler->filter('input[type="password"]');
        $this->assertEquals(1, $tag->count());
        $this->assertNull($tag->attr('value'));
    }

    /**
     * Testo il campo Hidden
     */
    public function testHiddenField(): void
    {
        $field = new HiddenField('test');
        $field->setGenerator($this->generator);

        // Il campo nascosto non ha il wrapper del twitter bootstrap
        $crawler = new Crawler($field->render());
        $tag = $crawler->filter('input[type="hidden"]');
        $this->assertEquals(1, $tag->count());

        $field->setValue('you can’t see me');
        $this->assertHiddenValue($field->render(), 'you can’t see me');

        $field->setReadonly();
        $this->assertHiddenValue($field->render(), 'you can’t see me', true);
    }

    /**
     * @param      $html
     * @param      $value
     * @param bool $readonly
     */
    protected function assertHiddenValue($html, $value, $readonly = false)
    {
        $crawler = new Crawler($html);
        $tag = $crawler->filter('input[type="hidden"]');

        if ($readonly) {
            // Se è readonly non deve comparire il valore da nessuna parte
            $this->assertDoesNotMatchRegularExpression('/' . $value . '/', $html);
        } else {
            $this->assertEquals($value, $tag->attr('value'));
        }
    }

    /**
     * Test di un campo RadioField
     */
    public function testRadioField(): void
    {
        $field = new RadioField('test');
        $field->setGenerator($this->generator);

        $values = ['a_value' => 'A', 'b_value' => 'B', 'c_value' => 'C', 'd_value' => 'D'];
        $field->setOptions($values);
        $field->setValue('b_value');

        $this->assertTagTwitterBootstrap($field->render());
        $this->assertRadioHasSelectedValue($field->render(), ['b_value' => 'B'], $values);

        $field->setReadonly();
        $this->assertRadioHasSelectedValue($field->render(), ['b_value' => 'B'], $values, true);
    }

    /**
     * @param       $html
     * @param bool  $readonly
     * @throws \InvalidArgumentException
     */
    protected function assertRadioHasSelectedValue($html, array $selected_value, array $values, $readonly = false)
    {
        if ($values === []) {
            throw new \InvalidArgumentException('Non è possibile testare le opzioni di un radio senza specificare i valori');
        }

        if (array_intersect_assoc($selected_value, $values) !== $selected_value) {
            throw new \InvalidArgumentException('L\'opzione selezionata non è presente nell\'array dei valori, controlla il test!');
        }

        $crawler = new Crawler($html);

        if ($readonly) {
            $radio = $crawler->filter('span');
            $this->assertTrue(in_array($radio->html(), $selected_value));
        } else {
            $radio = $crawler->filter('input[type="radio"]:checked');
            $this->assertArrayHasKey($radio->attr('value'), $selected_value);
        }

        $this->assertEquals(1, $radio->count(), $html);
    }

    public function testAdminStructure(): void
    {
        $config = new FormConfig('form', new ArrayRowSubject([]), new Localization());
        $config->addBaseStructure('main', '');
        $config->addTab('content/main', 'Main Test');
        $config->addPanel('content/main/panel');

        $structure = FormBuilder::buildConfig($config);
        $structure->setGenerator($this->generator);

        $html = $structure->render();

        $crawler = new Crawler($html);
        $this->assertEquals(1, $crawler->filter('.panels-container #tab_main fieldset.panel')->count());

        // Non devono esserci le tab con una tab sola
        $this->assertEquals(0, $crawler->filter('.panels-container ul.tabs')->count());

        $config->addTab('content/more_tabs', 'Just another tab in the wall');
        $structure = FormBuilder::buildConfig($config);

        $html = $structure->render();

        // Devono comparire le tab con più tab
        $crawler = new Crawler($html);
        $this->assertEquals(1, $crawler->filter('.panels-container ul.tabs')->count());
    }

    public function testAdminPanelsProperties(): void
    {
        $config = new FormConfig('form', new ArrayRowSubject([]), new Localization());
        $config->addBaseStructure('main', '');
        $config->addTab('content/main', 'Main Test');
        $config->addPanel('content/main/panel_A', null, false, ['description' => 'hello, this is description']);
        $config->addPanel('content/main/panel_A2', null, false, ['description' => '<div>hello, <strong>this</strong> is HTML description</div>']);
        $config->addPanel('content/main/panel_B', 'B has a Label');
        $config->addPanel('content/main/panel_C', 'C is Accordion', true);

        $structure = FormBuilder::buildConfig($config);
        $structure->setGenerator($this->generator);

        $html = $structure->render();
        $crawler = new Crawler($html);

        /**
         * Pannello normale con descrizione
         */
        $panel = $crawler->filter('#fieldset-form-panel_A.panel');
        $this->assertEquals(1, $panel->count());
        $this->assertEquals('hello, this is description', $panel->filter('p')->html());

        /**
         * Pannello normale con descrizione HTML
         */
        $panel = $crawler->filter('#fieldset-form-panel_A2.panel');
        $this->assertEquals(1, $panel->count());
        $this->assertEquals('hello, <strong>this</strong> is HTML description', $panel->filter('div')->html());

        /**
         * Pannello con intestazione
         */
        $panel = $crawler->filter('#fieldset-form-panel_B.panel');
        $this->assertEquals(1, $panel->count());
        $this->assertEquals(1, $panel->filter('h3')->count());
        $this->assertEquals('B has a Label', $panel->filter('h3')->html());

        /**
         * Accordion
         */
        $panel = $crawler->filter('#fieldset-form-panel_C.panel.panel-accordion');
        $this->assertEquals(1, $panel->count());
        $this->assertEquals(1, $panel->filter('h3')->count());
        $this->assertEquals('C is Accordion', $panel->filter('h3')->html());
    }

    public function testRepeater(): void
    {
        $config = new FormConfig('form', new ArrayRowSubject(['repeater' => ['row_1' => new ArrayRowSubject([])]]), new Localization());

        $config->addField(
            'repeater',
            [
                'class' => \Pongho\Form\Repeater\SerializableRepeater::class, // uno vale l'altro, cambia solo la gestione dei dati
            ]
        );

        $config->addRepeaterRow('repeater');

        $form = FormBuilder::buildConfig($config);
        $form->setGenerator($this->generator);

        $html = $form->render();
        $crawler = new Crawler($html);
        $repeater = $crawler->filter('#fieldset-form-repeater.repeater-container ol.repeater');

        /**
         * Struttura del repeater
         */
        $this->assertEquals(1, $repeater->count());
        $this->assertEquals(2, $repeater->filter('li.repeater-row')->count());

        /**
         * Presenza del tasto per l'aggiunta di nuove righe
         */
        $actions = $crawler->filter('div.actions');
        $this->assertEquals(1, $actions->count(), $html);

        $button = $actions->filter('a.repeater-add-row');
        $this->assertEquals(1, $button->count());
        $this->assertEquals('add_item', $button->html());

        /**
         * Presenza della blank row
         */
        $row = $repeater->filter('li#fieldset-form-repeater-row-new');
        $this->assertRepeaterRow($row, 'new');

        /**
         * Presenza della riga caricata da subject
         */
        $row = $repeater->filter('li#fieldset-form-repeater-row-row_1');
        $this->assertRepeaterRow($row, 'row_1');
    }

    protected function assertRepeaterRow(Crawler $row, $row_key)
    {
        $this->assertEquals(1, $row->count());
        $this->assertEquals($row_key, $row->attr('data-id'));

        if ($row_key === 'new') {
            $classes = explode(' ', (string) $row->attr('class'));
            $this->assertTrue(in_array('repeater-blank-row', $classes));
        }

        /**
         * Contenuto della riga
         */
        $this->assertEquals(1, $row->filter('div#repeater-form-repeater-' . $row_key . '-main.repeater-main')->count());
        $this->assertEquals(
            1,
            $row->filter('div#repeater-form-repeater-' . $row_key . '-content.repeater-content')->count()
        );
    }

    /**
     * @return array
     */
    public function selectValuesProvider()
    {
        return [
            ['', 'empty'],
            [0, 'zero'],
            [1, 'one'],
            [null, 'empty'],
            [false, 'zero'],
            [true, 'one'],
        ];
    }

    /**
     * @param $value
     * @param $content
     * @dataProvider selectValuesProvider
     */
    public function testReadonlyRenderParticularValues($value, $content): void
    {
        $select = new SelectField('test');
        $select->setGenerator($this->generator);
        $select->setReadonly();

        $options = [
            '' => 'empty',
            0  => 'zero',
            1  => 'one',
            // Questi valori non possono coesistere con i precedenti, verranno castati per coincidere con i precedenti
            // null  => 'null',
            // false => 'false',
            // true  => 'true',
        ];

        $select->setOptions($options);
        $select->setValue($value);

        $html = $select->render();

        $crawler = new Crawler($html);
        $this->assertEquals($content, $crawler->filter('.controls span')->html());
        $this->assertEquals($value, $crawler->filter('.controls span')->attr('data-value'));
    }
}
