<?php

declare(strict_types=1);

namespace Grav\Plugin\Api\Tests\Unit\Controllers;

use Grav\Plugin\Api\Controllers\AbstractApiController;
use Grav\Plugin\Api\Controllers\PagesController;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use Symfony\Component\Yaml\Yaml;

/**
 * Unit tests for AbstractApiController::mergePatch(), exercised through
 * PagesController (the page-update endpoint is where the regression bit).
 *
 * Regression coverage for getgrav/grav-theme-quark2#8 — saving a page whose
 * frontmatter contains a YAML list (e.g. `form.fields`, `form.process`) used to
 * grow the file on every save: array_replace_recursive merged the existing
 * integer-keyed list with the incoming name-keyed map, leaving both keysets in
 * the result and forcing Symfony YAML to dump the list as a quoted '0','1','2'
 * map next to the new named entries.
 */
#[CoversClass(AbstractApiController::class)]
#[CoversClass(PagesController::class)]
class PagesControllerMergeHeaderTest extends TestCase
{
    private function invoke(array $existing, array $incoming): array
    {
        $ref = new ReflectionClass(PagesController::class);
        $instance = $ref->newInstanceWithoutConstructor();

        return $ref->getMethod('mergePatch')->invoke($instance, $existing, $incoming);
    }

    #[Test]
    public function list_in_existing_is_replaced_when_incoming_sends_a_map(): void
    {
        $existing = [
            'form' => [
                'fields' => [
                    ['name' => 'name', 'label' => 'Name'],
                    ['name' => 'email', 'label' => 'Email'],
                    ['name' => 'message', 'label' => 'Message'],
                ],
            ],
        ];
        $incoming = [
            'form' => [
                'fields' => [
                    'name' => ['label' => 'Name'],
                    'email' => ['label' => 'Email'],
                ],
            ],
        ];

        $merged = $this->invoke($existing, $incoming);

        $this->assertSame(['name', 'email'], array_keys($merged['form']['fields']));
        $yaml = Yaml::dump($merged, 10, 2);
        $this->assertStringNotContainsString("'0':", $yaml, 'No quoted integer keys should leak through');
        $this->assertStringNotContainsString("'1':", $yaml);
        $this->assertStringNotContainsString("'2':", $yaml);
    }

    #[Test]
    public function list_in_incoming_replaces_existing_map(): void
    {
        $existing = [
            'form' => [
                'fields' => [
                    'name' => ['label' => 'Name'],
                    'email' => ['label' => 'Email'],
                ],
            ],
        ];
        $incoming = [
            'form' => [
                'fields' => [
                    ['name' => 'subject', 'label' => 'Subject'],
                ],
            ],
        ];

        $merged = $this->invoke($existing, $incoming);

        $this->assertTrue(array_is_list($merged['form']['fields']));
        $this->assertCount(1, $merged['form']['fields']);
        $this->assertSame('subject', $merged['form']['fields'][0]['name']);
    }

    #[Test]
    public function maps_still_recurse_so_partial_header_updates_keep_working(): void
    {
        $existing = [
            'title' => 'Old',
            'metadata' => [
                'description' => 'desc',
                'author' => 'andy',
            ],
            'published' => true,
        ];
        $incoming = [
            'metadata' => [
                'author' => 'someone',
            ],
        ];

        $merged = $this->invoke($existing, $incoming);

        $this->assertSame('Old', $merged['title']);
        $this->assertSame('desc', $merged['metadata']['description']);
        $this->assertSame('someone', $merged['metadata']['author']);
        $this->assertTrue($merged['published']);
    }

    #[Test]
    public function nested_list_under_a_map_is_replaced_not_index_merged(): void
    {
        $existing = ['taxonomy' => ['tag' => ['a', 'b', 'c', 'd']]];
        $incoming = ['taxonomy' => ['tag' => ['x', 'y']]];

        $merged = $this->invoke($existing, $incoming);

        $this->assertSame(['x', 'y'], $merged['taxonomy']['tag']);
    }

    #[Test]
    public function quark2_issue_8_full_payload_round_trip(): void
    {
        $existing = [
            'title' => 'Contact',
            'form' => [
                'name' => 'contact-form',
                'fields' => [
                    ['name' => 'name', 'label' => 'Name', 'type' => 'text'],
                    ['name' => 'email', 'label' => 'Email', 'type' => 'email'],
                    ['name' => 'message', 'label' => 'Message', 'type' => 'textarea'],
                ],
                'buttons' => [
                    ['type' => 'submit', 'value' => 'Submit'],
                    ['type' => 'reset', 'value' => 'Reset'],
                ],
                'process' => [
                    ['email' => ['from' => 'x', 'subject' => 'y']],
                    ['save' => ['fileprefix' => 'feedback-']],
                    ['message' => 'Thank you'],
                    ['display' => '/contact'],
                ],
            ],
        ];
        $incoming = [
            'form' => [
                'fields' => [
                    'name' => ['label' => 'Name', 'type' => 'text'],
                    'email' => ['label' => 'Email', 'type' => 'email'],
                ],
                'buttons' => [
                    'submit' => ['type' => 'submit', 'value' => 'Submit'],
                ],
                'process' => [
                    'email' => ['from' => 'x', 'subject' => 'y'],
                    'message' => 'Thank you',
                ],
            ],
        ];

        $merged = $this->invoke($existing, $incoming);

        $this->assertSame('Contact', $merged['title']);
        $this->assertSame('contact-form', $merged['form']['name']);
        $this->assertSame(['name', 'email'], array_keys($merged['form']['fields']));
        $this->assertSame(['submit'], array_keys($merged['form']['buttons']));
        $this->assertSame(['email', 'message'], array_keys($merged['form']['process']));

        $yaml = Yaml::dump($merged, 10, 2);
        $this->assertStringNotContainsString("'0':", $yaml);
        $this->assertStringNotContainsString("'1':", $yaml);
        $this->assertStringNotContainsString("'2':", $yaml);
        $this->assertStringNotContainsString("'3':", $yaml);
    }

    #[Test]
    public function a_file_removed_from_a_file_field_does_not_come_back(): void
    {
        $existing = [
            'hero' => [
                'user/pages/01.home/a.jpg' => ['name' => 'a.jpg', 'type' => 'image/jpeg', 'size' => 10, 'path' => 'user/pages/01.home/a.jpg'],
                'user/pages/01.home/b.jpg' => ['name' => 'b.jpg', 'type' => 'image/jpeg', 'size' => 20, 'path' => 'user/pages/01.home/b.jpg'],
            ],
            'title' => 'Home',
        ];
        // The admin sends the field's full remaining set after b.jpg is removed.
        $incoming = [
            'hero' => [
                'user/pages/01.home/a.jpg' => ['name' => 'a.jpg', 'type' => 'image/jpeg', 'size' => 10, 'path' => 'user/pages/01.home/a.jpg'],
            ],
        ];

        $merged = $this->invoke($existing, $incoming);

        $this->assertSame(['user/pages/01.home/a.jpg'], array_keys($merged['hero']));
        $this->assertSame('Home', $merged['title']);
    }

    #[Test]
    public function swapping_a_file_replaces_the_old_entry(): void
    {
        $existing = [
            'logo' => ['user/pages/01.home/old.png' => ['name' => 'old.png', 'path' => 'user/pages/01.home/old.png']],
        ];
        $incoming = [
            'logo' => ['user/pages/01.home/new.png' => ['name' => 'new.png', 'path' => 'user/pages/01.home/new.png']],
        ];

        $merged = $this->invoke($existing, $incoming);

        $this->assertSame(['user/pages/01.home/new.png'], array_keys($merged['logo']));
    }

    #[Test]
    public function ordinary_nested_maps_still_merge(): void
    {
        $existing = ['taxonomy' => ['category' => ['news'], 'tag' => ['a']], 'og' => ['title' => 'T', 'image' => 'x.jpg']];
        $incoming = ['og' => ['title' => 'New title']];

        $merged = $this->invoke($existing, $incoming);

        $this->assertSame(['title' => 'New title', 'image' => 'x.jpg'], $merged['og']);
        $this->assertSame(['category' => ['news'], 'tag' => ['a']], $merged['taxonomy']);
    }
}
