<?php

/**
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * https://www.d3data.de
 *
 * @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
 * @author    D3 Data Development - Daniel Seifert <info@shopmodule.com>
 * @link      https://www.oxidmodule.com
 */

declare(strict_types=1);

namespace D3\DataWizard\tests\unit\Application\Model;

use D3\DataWizard\Application\Model\Exceptions\ExportFileException;
use D3\DataWizard\Application\Model\Exceptions\TaskException;
use D3\DataWizard\Application\Model\ExportRenderer\Csv;
use D3\DataWizard\Application\Model\ExportRenderer\RendererBridge;
use D3\DataWizard\Application\Model\ExportRenderer\RendererInterface;
use D3\DataWizard\tests\tools\d3TestExport;
use D3\ModCfg\Application\Model\d3filesystem;
use D3\ModCfg\Tests\unit\d3ModCfgUnitTestCase;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\Result;
use FormManager\Inputs\Hidden;
use FormManager\Inputs\Number;
use FormManager\Inputs\Radio;
use OxidEsales\Eshop\Core\Database\Adapter\Doctrine\Database;
use OxidEsales\Eshop\Core\Exception\StandardException;
use PHPUnit\Framework\MockObject\MockObject;
use ReflectionException;

class ExportBaseTest extends d3ModCfgUnitTestCase
{
    /** @var d3TestExport */
    protected $_oModel;

    public function setUp(): void
    {
        parent::setUp();

        $this->_oModel = oxNew(d3TestExport::class);
    }

    public function tearDown(): void
    {
        parent::tearDown();

        unset($this->_oModel);
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getDescription
     * @test
     * @throws ReflectionException
     */
    public function canGetDescription()
    {
        $this->assertIsString(
            $this->callMethod(
                $this->_oModel,
                'getDescription'
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getButtonText
     * @test
     * @throws ReflectionException
     */
    public function canGetButtonText()
    {
        $this->assertIsString(
            $this->callMethod(
                $this->_oModel,
                'getButtonText'
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::hasFormElements
     * @test
     * @throws ReflectionException
     * @dataProvider canGetHasFormElementsDataProvider
     */
    public function canGetHasFormElements($formElements, $expected)
    {
        $this->setValue($this->_oModel, 'formElements', $formElements);

        $this->assertSame(
            $expected,
            $this->callMethod(
                $this->_oModel,
                'hasFormElements'
            )
        );
    }

    public function canGetHasFormElementsDataProvider(): array
    {
        return [
            'hasFormElements'   => [['abc', 'def'], true],
            'hasNoFormElements' => [[], false],
        ];
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getFormElements
     * @test
     * @throws ReflectionException
     * @dataProvider canGetHasFormElementsDataProvider
     */
    public function canGetFormElements($formElements)
    {
        $this->setValue($this->_oModel, 'formElements', $formElements);

        $this->assertSame(
            $formElements,
            $this->callMethod(
                $this->_oModel,
                'getFormElements'
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::registerFormElement
     * @test
     * @throws ReflectionException
     * @dataProvider canRegisterFormElementDataProvider
     */
    public function canRegisterFormElement($inputClass)
    {
        $oldCount = count($this->getValue($this->_oModel, 'formElements'));

        /** @var Radio|MockObject $inputMock */
        $inputMock = $this->getMockBuilder($inputClass)
            ->onlyMethods([
                'setTemplate',
                'setAttribute',
            ])
            ->getMock();
        $inputMock->expects($this->atLeastOnce())->method('setTemplate');
        $inputMock->expects($this->atLeastOnce())->method('setAttribute');

        $this->callMethod(
            $this->_oModel,
            'registerFormElement',
            [$inputMock]
        );

        $newCount = count($this->getValue($this->_oModel, 'formElements'));

        $this->assertGreaterThan($oldCount, $newCount);
    }

    /**
     * @return \string[][]
     */
    public function canRegisterFormElementDataProvider(): array
    {
        return [
            'Radio' => [Radio::class],
            'Checkbox' => [Radio::class],
            'Hidden' => [Hidden::class],
        ];
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::run
     * @test
     * @throws ReflectionException
     */
    public function canRunWithoutFormElements()
    {
        $format = 'myFormat';
        $path = 'myPath';

        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'hasFormElements',
                'executeExport',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('hasFormElements')->willReturn(false);
        $modelMock->expects($this->atLeastOnce())->method('executeExport')->with($format, $path)->willReturn('some content');
        $this->_oModel = $modelMock;

        $this->callMethod(
            $this->_oModel,
            'run',
            [$format, $path]
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::run
     * @test
     * @throws ReflectionException
     * @dataProvider canRunWithFormElementsDataProvider
     */
    public function canRunWithFormElements($elements, $blThrowException)
    {
        $format = 'myFormat';
        $path = 'myPath';

        $expectedException = oxNew(StandardException::class);

        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'hasFormElements',
                'executeExport',
                'getFormElements',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('hasFormElements')->willReturn(true);
        $modelMock->expects($this->exactly((int) !$blThrowException))->method('executeExport')->with($format, $path)->willReturn('some content');
        $modelMock->expects($this->atLeastOnce())->method('getFormElements')->willReturn($elements);
        $this->_oModel = $modelMock;

        if ($blThrowException) {
            $this->expectException(get_class($expectedException));
        }

        $this->callMethod(
            $this->_oModel,
            'run',
            [$format, $path]
        );
    }

    /**
     * @return array[]
     */
    public function canRunWithFormElementsDataProvider(): array
    {
        /** @var Radio|MockObject $validMock */
        $validMock = $this->getMockBuilder(Radio::class)
            ->onlyMethods(['isValid'])
            ->getMock();
        $validMock->expects($this->atLeastOnce())->method('isValid')->willReturn(true);

        $invalidField = new Number(null, [
            'required' => true,
            'min' => 1,
            'max' => 10,
            'step' => 5,
        ]);
        $invalidField
            ->setValue(20)
            ->setErrorMessages(['errorMsgs']);

        return [
            'validElements' => [[$validMock, $validMock], false],
            'invalidElements' => [[$validMock, $invalidField], true],
        ];
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::executeExport
     * @test
     * @throws ReflectionException
     * @dataProvider canExecuteExportDataProvider
     */
    public function canExecuteExport($path, $throwsException)
    {
        /** @var d3filesystem|MockObject $fsMock */
        $fsMock = $this->getMockBuilder(d3filesystem::class)
            ->onlyMethods([
                'startDirectDownload',
                'filterFilename',
                'trailingslashit',
                'createFile',
            ])
            ->getMock();
        $fsMock->expects($this->exactly((int) !isset($path)))->method('startDirectDownload')->willReturn(true);
        $fsMock->method('filterFilename')->willReturnArgument(0);
        $fsMock->expects($this->exactly((int) isset($path)))->method('trailingslashit')->willReturnArgument(0);
        $fsMock->expects($this->exactly((int) isset($path)))->method('createFile')->willReturn((bool) !$throwsException);

        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'getContent',
                'getFileSystem',
                'getExportFileName',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('getContent')->willReturn('some content');
        $modelMock->expects($this->atLeastOnce())->method('getFileSystem')->willReturn($fsMock);
        $modelMock->expects($this->atLeastOnce())->method('getExportFileName')->willReturn('exportFileName');

        $this->_oModel = $modelMock;

        if ($path && $throwsException) {
            $this->expectException(ExportFileException::class);
        }

        $this->callMethod(
            $this->_oModel,
            'executeExport',
            ['CSV', $path]
        );
    }

    /**
     * @return array[]
     */
    public function canExecuteExportDataProvider(): array
    {
        return [
            'unable to create path for saving file'   => ['myPath', true],
            'can create path for saving file'   => ['myPath', false],
            'no path for download'   => [null, false],
        ];
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getConnection
     * @test
     * @throws ReflectionException
     */
    public function canGetConnection()
    {
        $this->assertInstanceOf(
            Connection::class,
            $this->callMethod(
                $this->_oModel,
                'getConnection'
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getFileSystem
     * @test
     * @throws ReflectionException
     */
    public function canGetFileSystem()
    {
        $this->assertInstanceOf(
            d3filesystem::class,
            $this->callMethod(
                $this->_oModel,
                'getFileSystem'
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getRenderer
     * @test
     * @throws ReflectionException
     */
    public function canGetRenderer()
    {
        /** @var RendererInterface|MockObject $rendererMock */
        $rendererMock = $this->getMockBuilder(Csv::class)
            ->getMock();

        /** @var RendererBridge|MockObject $rendererBridgeMock */
        $rendererBridgeMock = $this->getMockBuilder(RendererBridge::class)
            ->onlyMethods(['getRenderer'])
            ->getMock();
        $rendererBridgeMock->expects($this->atLeastOnce())->method('getRenderer')->willReturn($rendererMock);

        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'getRendererBridge',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('getRendererBridge')->willReturn($rendererBridgeMock);

        $this->_oModel = $modelMock;

        $this->assertSame(
            $rendererMock,
            $this->callMethod(
                $this->_oModel,
                'getRenderer',
                [RendererBridge::FORMAT_CSV]
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getRendererBridge
     * @test
     * @throws ReflectionException
     */
    public function canGetRendererBridge()
    {
        $this->assertInstanceOf(
            RendererBridge::class,
            $this->callMethod(
                $this->_oModel,
                'getRendererBridge'
            )
        );
    }



    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getFileExtension
     * @test
     * @throws ReflectionException
     */
    public function canGetFileExtension()
    {
        $format = RendererBridge::FORMAT_CSV;
        $expected = 'myFileExtension';

        /** @var RendererInterface|MockObject $rendererMock */
        $rendererMock = $this->getMockBuilder(Csv::class)
            ->onlyMethods(['getFileExtension'])
            ->getMock();
        $rendererMock->expects($this->atLeastOnce())->method('getFileExtension')->willReturn($expected);

        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'getRenderer',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('getRenderer')->with($format)->willReturn($rendererMock);

        $this->_oModel = $modelMock;

        $this->assertSame(
            $expected,
            $this->callMethod(
                $this->_oModel,
                'getFileExtension',
                [$format]
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::renderContent
     * @test
     * @throws ReflectionException
     */
    public function canRenderContent()
    {
        $rows = ['row1', 'row2'];
        $fieldnames = ['fieldname1', 'fieldname2'];
        $format = RendererBridge::FORMAT_CSV;
        $expected = 'myContent';

        /** @var RendererInterface|MockObject $rendererMock */
        $rendererMock = $this->getMockBuilder(Csv::class)
            ->onlyMethods(['getContent'])
            ->getMock();
        $rendererMock->expects($this->atLeastOnce())->method('getContent')->with($rows, $fieldnames)->willReturn($expected);

        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'getRenderer',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('getRenderer')->with($format)->willReturn($rendererMock);

        $this->_oModel = $modelMock;

        $this->assertSame(
            $expected,
            $this->callMethod(
                $this->_oModel,
                'renderContent',
                [$rows, $fieldnames, $format]
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getExportFilenameBase
     * @test
     * @throws ReflectionException
     */
    public function canGetExportFilenameBase()
    {
        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'getTitle',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('getTitle')->willReturn('someTitle');

        $this->_oModel = $modelMock;

        $this->callMethod(
            $this->_oModel,
            'getExportFilenameBase'
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getExportFileName
     * @test
     * @throws ReflectionException
     */
    public function canGetExportFileName()
    {
        $format = RendererBridge::FORMAT_CSV;

        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'getExportFilenameBase',
                'getFileExtension',
            ])
            ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('getExportFilenameBase')->willReturn('base');
        $modelMock->expects($this->atLeastOnce())->method('getFileExtension')->with($format)->willReturn('extension');

        $this->_oModel = $modelMock;

        $this->assertMatchesRegularExpression(
            '/^base_(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.extension$/m',
            $this->callMethod(
                $this->_oModel,
                'getExportFileName',
                [$format]
            )
        );
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getExportData
     * @test
     * @throws ReflectionException
     * @dataProvider canGetExportDataDataProvider
     */
    public function canGetExportData($query, $throwsException, $dbResult)
    {
        /** @var Result|MockObject $resultMock */
        $resultMock = $this->getMockBuilder(Result::class)
            ->onlyMethods(get_class_methods(Result::class))
            ->getMock();
        $resultMock->method('fetchAllAssociative')->willReturn($dbResult);

        /** @var Database|MockObject $connectionMock */
        $connectionMock = $this->getMockBuilder(Connection::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['executeQuery'])
            ->getMock();
        $connectionMock->expects($this->exactly((int) !$throwsException))->method('executeQuery')->willReturn($resultMock);

        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
            ->onlyMethods([
                'getConnection',
            ])
            ->getMock();
        $modelMock->expects($this->exactly((int) !$throwsException))->method('getConnection')->willReturn($connectionMock);

        $this->_oModel = $modelMock;

        try {
            $result = $this->callMethod(
                $this->_oModel,
                'getExportData',
                [[$query], ['param1', 'param2']]
            );

            $this->assertSame(
                [
                    [
                        [
                            'field1'    => 'content1',
                            'field2'    => 'content2',
                        ],
                    ],
                    [
                        'field1',
                        'field2',
                    ],
                ],
                $result
            );
        } catch (TaskException $e) {
            if ($throwsException) {
                $this->assertStringContainsString('NOEXPORTSELECT', $e->getMessage());
            } elseif (!count($dbResult)) {
                $this->assertStringContainsString('kein Inhalt', $e->getMessage());
            }
        }
    }

    /**
     * @return array[]
     */
    public function canGetExportDataDataProvider(): array
    {
        return [
            'not SELECT throws exception'   => [' UPDATE 1', true, []],
            'empty SELECT'   => [' SELECT 1', false, []],
            'fulfilled SELECT'   => [' SELECT 1', false, [['field1' => 'content1', 'field2' => 'content2']]],
        ];
    }

    /**
     * @covers \D3\DataWizard\Application\Model\ExportBase::getContent
     * @test
     * @throws ReflectionException
     */
    public function canGetContent()
    {
        /** @var d3TestExport|MockObject $modelMock */
        $modelMock = $this->getMockBuilder(d3TestExport::class)
        ->onlyMethods([
            'getQuery',
            'getExportData',
            'renderContent',
        ])
        ->getMock();
        $modelMock->expects($this->atLeastOnce())->method('getQuery')->willReturn(['SELECT 1', ['arg1', 'arg2']]);
        $modelMock->expects($this->atLeastOnce())->method('getExportData')->willReturn([[1, 2], ['field1', 'field2']]);
        $modelMock->expects($this->atLeastOnce())->method('renderContent')->willReturn('some content');

        $this->_oModel = $modelMock;

        $this->assertSame(
            'some content',
            $this->callMethod(
                $this->_oModel,
                'getContent',
                ['CSV']
            )
        );
    }
}