<?php

declare(strict_types=1);

namespace Sabberworm\CSS\Tests\Unit;

use PHPUnit\Framework\TestCase;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\OutputFormatter;
use Sabberworm\CSS\Renderable;

/**
 * @covers \Sabberworm\CSS\OutputFormatter
 */
final class OutputFormatterTest extends TestCase
{
    /**
     * @var OutputFormatter
     */
    private $subject;

    /**
     * @var OutputFormat
     */
    private $outputFormat;

    protected function setUp(): void
    {
        $this->outputFormat = new OutputFormat();
        $this->subject = new OutputFormatter($this->outputFormat);
    }

    /**
     * @test
     */
    public function spaceAfterRuleNameReturnsSpaceAfterRuleNameFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceAfterRuleName($space);

        self::assertSame($space, $this->subject->spaceAfterRuleName());
    }

    /**
     * @test
     */
    public function spaceBeforeRulesReturnsSpaceBeforeRulesFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceBeforeRules($space);

        self::assertSame($space, $this->subject->spaceBeforeRules());
    }

    /**
     * @test
     */
    public function spaceAfterRulesReturnsSpaceAfterRulesFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceAfterRules($space);

        self::assertSame($space, $this->subject->spaceAfterRules());
    }

    /**
     * @test
     */
    public function spaceBetweenRulesReturnsSpaceBetweenRulesFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceBetweenRules($space);

        self::assertSame($space, $this->subject->spaceBetweenRules());
    }

    /**
     * @test
     */
    public function spaceBeforeBlocksReturnsSpaceBeforeBlocksFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceBeforeBlocks($space);

        self::assertSame($space, $this->subject->spaceBeforeBlocks());
    }

    /**
     * @test
     */
    public function spaceAfterBlocksReturnsSpaceAfterBlocksFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceAfterBlocks($space);

        self::assertSame($space, $this->subject->spaceAfterBlocks());
    }

    /**
     * @test
     */
    public function spaceBetweenBlocksReturnsSpaceBetweenBlocksFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceBetweenBlocks($space);

        self::assertSame($space, $this->subject->spaceBetweenBlocks());
    }

    /**
     * @test
     */
    public function spaceBeforeSelectorSeparatorReturnsSpaceBeforeSelectorSeparatorFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceBeforeSelectorSeparator($space);

        self::assertSame($space, $this->subject->spaceBeforeSelectorSeparator());
    }

    /**
     * @test
     */
    public function spaceAfterSelectorSeparatorReturnsSpaceAfterSelectorSeparatorFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceAfterSelectorSeparator($space);

        self::assertSame($space, $this->subject->spaceAfterSelectorSeparator());
    }

    /**
     * @test
     */
    public function spaceBeforeListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void
    {
        $separator = ',';
        $space = '        ';
        $this->outputFormat->setSpaceBeforeListArgumentSeparators([$separator => $space]);
        $defaultSpace = "\t\t\t\t";
        $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace);

        self::assertSame($space, $this->subject->spaceBeforeListArgumentSeparator($separator));
    }

    /**
     * @test
     */
    public function spaceBeforeListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceBeforeListArgumentSeparators([',' => $space]);
        $defaultSpace = "\t\t\t\t";
        $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace);

        self::assertSame($defaultSpace, $this->subject->spaceBeforeListArgumentSeparator(';'));
    }

    /**
     * @test
     */
    public function spaceAfterListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void
    {
        $separator = ',';
        $space = '        ';
        $this->outputFormat->setSpaceAfterListArgumentSeparators([$separator => $space]);
        $defaultSpace = "\t\t\t\t";
        $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace);

        self::assertSame($space, $this->subject->spaceAfterListArgumentSeparator($separator));
    }

    /**
     * @test
     */
    public function spaceAfterListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceAfterListArgumentSeparators([',' => $space]);
        $defaultSpace = "\t\t\t\t";
        $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace);

        self::assertSame($defaultSpace, $this->subject->spaceAfterListArgumentSeparator(';'));
    }

    /**
     * @test
     */
    public function spaceBeforeOpeningBraceReturnsSpaceBeforeOpeningBraceFromOutputFormat(): void
    {
        $space = '        ';
        $this->outputFormat->setSpaceBeforeOpeningBrace($space);

        self::assertSame($space, $this->subject->spaceBeforeOpeningBrace());
    }

    /**
     * @test
     */
    public function implodeForEmptyValuesReturnsEmptyString(): void
    {
        $values = [];

        $result = $this->subject->implode(', ', $values);

        self::assertSame('', $result);
    }

    /**
     * @test
     */
    public function implodeWithOneStringValueReturnsStringValue(): void
    {
        $value = 'tea';
        $values = [$value];

        $result = $this->subject->implode(', ', $values);

        self::assertSame($value, $result);
    }

    /**
     * @test
     */
    public function implodeWithMultipleStringValuesReturnsValuesSeparatedBySeparator(): void
    {
        $value1 = 'tea';
        $value2 = 'coffee';
        $values = [$value1, $value2];
        $separator = ', ';

        $result = $this->subject->implode($separator, $values);

        self::assertSame($value1 . $separator . $value2, $result);
    }

    /**
     * @test
     */
    public function implodeWithOneRenderableReturnsRenderedRenderable(): void
    {
        $renderable = $this->createMock(Renderable::class);
        $renderedRenderable = 'tea';
        $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable);
        $values = [$renderable];

        $result = $this->subject->implode(', ', $values);

        self::assertSame($renderedRenderable, $result);
    }

    /**
     * @test
     */
    public function implodeWithMultipleRenderablesReturnsRenderedRenderablesSeparatedBySeparator(): void
    {
        $renderable1 = $this->createMock(Renderable::class);
        $renderedRenderable1 = 'tea';
        $renderable1->method('render')->with($this->outputFormat)->willReturn($renderedRenderable1);
        $renderable2 = $this->createMock(Renderable::class);
        $renderedRenderable2 = 'coffee';
        $renderable2->method('render')->with($this->outputFormat)->willReturn($renderedRenderable2);
        $values = [$renderable1, $renderable2];
        $separator = ', ';

        $result = $this->subject->implode($separator, $values);

        self::assertSame($renderedRenderable1 . $separator . $renderedRenderable2, $result);
    }

    /**
     * @test
     */
    public function implodeWithIncreaseLevelFalseUsesDefaultIndentationLevelForRendering(): void
    {
        $renderable = $this->createMock(Renderable::class);
        $renderedRenderable = 'tea';
        $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable);
        $values = [$renderable];

        $result = $this->subject->implode(', ', $values, false);

        self::assertSame($renderedRenderable, $result);
    }

    /**
     * @test
     */
    public function implodeWithIncreaseLevelTrueIncreasesIndentationLevelForRendering(): void
    {
        $renderable = $this->createMock(Renderable::class);
        $renderedRenderable = 'tea';
        $renderable->method('render')->with($this->outputFormat->nextLevel())->willReturn($renderedRenderable);
        $values = [$renderable];

        $result = $this->subject->implode(', ', $values, true);

        self::assertSame($renderedRenderable, $result);
    }

    /**
     * @return array<string, array{0: string}>
     */
    public function provideUnchangedStringForRemoveLastSemicolon(): array
    {
        return [
            'empty string' => [''],
            'string without semicolon' => ['earl-grey: hot'],
            'string with trailing semicolon' => ['Earl Grey: hot;'],
            'string with semicolon in the middle' => ['Earl Grey: hot; Coffee: Americano'],
            'string with semicolons in the middle and trailing' => ['Earl Grey: hot; Coffee: Americano;'],
        ];
    }

    /**
     * @test
     * @dataProvider provideUnchangedStringForRemoveLastSemicolon
     */
    public function removeLastSemicolonWithSemicolonAfterLastRuleEnabledReturnsUnchangedArgument(string $string): void
    {
        $this->outputFormat->setSemicolonAfterLastRule(true);

        $result = $this->subject->removeLastSemicolon($string);

        self::assertSame($string, $result);
    }

    /**
     * @return array<string, array{0: string, 1: string}>
     */
    public function provideChangedStringForRemoveLastSemicolon(): array
    {
        return [
            'empty string' => ['', ''],
            'non-empty string without semicolon' => ['Earl Grey: hot', 'Earl Grey: hot'],
            'just 1 semicolon' => [';', ''],
            'just 2 semicolons' => [';;', ';'],
            'string with trailing semicolon' => ['Earl Grey: hot;', 'Earl Grey: hot'],
            'string with semicolon in the middle' => [
                'Earl Grey: hot; Coffee: Americano',
                'Earl Grey: hot Coffee: Americano',
            ],
            'string with semicolon in the middle and trailing' => [
                'Earl Grey: hot; Coffee: Americano;',
                'Earl Grey: hot; Coffee: Americano',
            ],
            'string with 2 semicolons in the middle' => ['tea; coffee; Club-Mate', 'tea; coffee Club-Mate'],
            'string with 2 semicolons in the middle surrounded by spaces' => [
                'Earl Grey: hot ; Coffee: Americano ; Club-Mate: cold',
                'Earl Grey: hot ; Coffee: Americano  Club-Mate: cold',
            ],
            'string with 2 adjacent semicolons in the middle' => [
                'Earl Grey: hot;; Coffee: Americano',
                'Earl Grey: hot; Coffee: Americano',
            ],
            'string with 3 adjacent semicolons in the middle' => [
                'Earl Grey: hot;;; Coffee: Americano',
                'Earl Grey: hot;; Coffee: Americano',
            ],
        ];
    }

    /**
     * @test
     * @dataProvider provideChangedStringForRemoveLastSemicolon
     */
    public function removeLastSemicolonWithSemicolonAfterLastRuleDisabledRemovesLastSemicolon(
        string $input,
        string $expected
    ): void {
        $this->outputFormat->setSemicolonAfterLastRule(false);

        $result = $this->subject->removeLastSemicolon($input);

        self::assertSame($expected, $result);
    }

    /**
     * @test
     */
    public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceBetweenBlocks(): void
    {
        $this->outputFormat->setRenderComments(false);
        $spaceBetweenBlocks = ' between-space ';
        $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks);

        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([]);

        $result = $this->subject->comments($commentable);

        self::assertStringNotContainsString($spaceBetweenBlocks, $result);
    }

    /**
     * @test
     */
    public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceAfterBlocks(): void
    {
        $this->outputFormat->setRenderComments(false);
        $spaceAfterBlocks = ' after-space ';
        $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks);

        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([]);

        $result = $this->subject->comments($commentable);

        self::assertStringNotContainsString($spaceAfterBlocks, $result);
    }

    /**
     * @test
     */
    public function commentsWithEmptyCommentableAndRenderCommentsDisabledReturnsEmptyString(): void
    {
        $this->outputFormat->setRenderComments(false);

        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([]);

        $result = $this->subject->comments($commentable);

        self::assertSame('', $result);
    }

    /**
     * @test
     */
    public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceBetweenBlocks(): void
    {
        $this->outputFormat->setRenderComments(true);
        $spaceBetweenBlocks = ' between-space ';
        $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks);

        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([]);

        $result = $this->subject->comments($commentable);

        self::assertStringNotContainsString($spaceBetweenBlocks, $result);
    }

    /**
     * @test
     */
    public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceAfterBlocks(): void
    {
        $this->outputFormat->setRenderComments(true);
        $spaceAfterBlocks = ' after-space ';
        $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks);

        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([]);

        $result = $this->subject->comments($commentable);

        self::assertStringNotContainsString($spaceAfterBlocks, $result);
    }

    /**
     * @test
     */
    public function commentsWithEmptyCommentableAndRenderCommentsEnabledReturnsEmptyString(): void
    {
        $this->outputFormat->setRenderComments(true);

        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([]);

        $result = $this->subject->comments($commentable);

        self::assertSame('', $result);
    }

    /**
     * @test
     */
    public function commentsWithCommentableWithOneCommentAndRenderCommentsDisabledReturnsEmptyString(): void
    {
        $this->outputFormat->setRenderComments(false);

        $commentText = 'I am a teapot.';
        $comment = new Comment($commentText);
        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([$comment]);

        $result = $this->subject->comments($commentable);

        self::assertSame('', $result);
    }

    /**
     * @test
     */
    public function commentsWithCommentableWithOneCommentRendersComment(): void
    {
        $this->outputFormat->setRenderComments(true);

        $commentText = 'I am a teapot.';
        $comment = new Comment($commentText);
        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([$comment]);

        $result = $this->subject->comments($commentable);

        self::assertStringContainsString('/*' . $commentText . '*/', $result);
    }

    /**
     * @test
     */
    public function commentsWithCommentableWithOneCommentPutsSpaceAfterBlocksAfterRenderedComment(): void
    {
        $this->outputFormat->setRenderComments(true);
        $afterSpace = ' after-space ';
        $this->outputFormat->setSpaceAfterBlocks($afterSpace);

        $commentText = 'I am a teapot.';
        $comment = new Comment($commentText);
        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([$comment]);

        $result = $this->subject->comments($commentable);

        self::assertSame('/*' . $commentText . '*/' . $afterSpace, $result);
    }

    /**
     * @test
     */
    public function commentsWithCommentableWithTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void
    {
        $this->outputFormat->setRenderComments(true);
        $afterSpace = ' after-space ';
        $this->outputFormat->setSpaceAfterBlocks($afterSpace);

        $commentText1 = 'I am a teapot.';
        $comment1 = new Comment($commentText1);
        $commentText2 = 'But I am not.';
        $comment2 = new Comment($commentText2);
        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([$comment1, $comment2]);

        $result = $this->subject->comments($commentable);

        self::assertStringContainsString('/*' . $commentText2 . '*/' . $afterSpace, $result);
    }

    /**
     * @test
     */
    public function commentsWithCommentableWithTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void
    {
        $this->outputFormat->setRenderComments(true);
        $betweenSpace = ' between-space ';
        $this->outputFormat->setSpaceBetweenBlocks($betweenSpace);

        $commentText1 = 'I am a teapot.';
        $comment1 = new Comment($commentText1);
        $commentText2 = 'But I am not.';
        $comment2 = new Comment($commentText2);
        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([$comment1, $comment2]);

        $result = $this->subject->comments($commentable);

        $expected = '/*' . $commentText1 . '*/' . $betweenSpace . '/*' . $commentText2 . '*/';
        self::assertStringContainsString($expected, $result);
    }

    /**
     * @test
     */
    public function commentsWithCommentableWithMoreThanTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void
    {
        $this->outputFormat->setRenderComments(true);
        $afterSpace = ' after-space ';
        $this->outputFormat->setSpaceAfterBlocks($afterSpace);

        $commentText1 = 'I am a teapot.';
        $comment1 = new Comment($commentText1);
        $commentText2 = 'But I am not.';
        $comment2 = new Comment($commentText2);
        $commentText3 = 'So what am I then?';
        $comment3 = new Comment($commentText3);
        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]);

        $result = $this->subject->comments($commentable);

        self::assertStringContainsString('/*' . $commentText3 . '*/' . $afterSpace, $result);
    }

    /**
     * @test
     */
    public function commentsWithCommentableWithMoreThanTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void
    {
        $this->outputFormat->setRenderComments(true);
        $betweenSpace = ' between-space ';
        $this->outputFormat->setSpaceBetweenBlocks($betweenSpace);

        $commentText1 = 'I am a teapot.';
        $comment1 = new Comment($commentText1);
        $commentText2 = 'But I am not.';
        $comment2 = new Comment($commentText2);
        $commentText3 = 'So what am I then?';
        $comment3 = new Comment($commentText3);
        $commentable = $this->createMock(Commentable::class);
        $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]);

        $result = $this->subject->comments($commentable);

        $expected = '/*' . $commentText1 . '*/'
            . $betweenSpace . '/*' . $commentText2 . '*/'
            . $betweenSpace . '/*' . $commentText3 . '*/';
        self::assertStringContainsString($expected, $result);
    }
}