<?php

namespace NoahVet\Reef\Infrastructure\Phpunit\Response;

use NoahVet\Reef\Contract\Phpunit\EndpointTestResponseInterface;
use NoahVet\Reef\Domain\Tool\ArrayTool;
use PHPUnit\Framework\Assert;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;

class EndpointTestResponse implements EndpointTestResponseInterface
{
    /** @var array<string, mixed> */
    protected readonly array $decodedBody;

    /**
     * @param array<mixed>|null $expectedBody
     * @param list<string>      $blacklist
     */
    public function __construct(
        protected readonly Response $response,
        protected readonly ?array $expectedBody,
        protected readonly array $blacklist,
    ) {
        $content = $response->getContent();
        $this->decodedBody = \json_decode(false !== $content ? $content : '[]', true) ?? [];
    }

    #[\Override]
    public function assertStatus(int $code): static
    {
        Assert::assertSame($code, $this->response->getStatusCode());

        return $this;
    }

    #[\Override]
    public function assertOk(): static
    {
        return $this->assertStatus(Response::HTTP_OK);
    }

    public function assertBadRequest(): static
    {
        return $this->assertStatus(Response::HTTP_BAD_REQUEST);
    }

    #[\Override]
    public function assertCreated(): static
    {
        return $this->assertStatus(Response::HTTP_CREATED);
    }

    #[\Override]
    public function assertNoContent(): static
    {
        return $this->assertStatus(Response::HTTP_NO_CONTENT);
    }

    #[\Override]
    public function assertUnauthorized(): static
    {
        return $this->assertStatus(Response::HTTP_UNAUTHORIZED);
    }

    #[\Override]
    public function assertForbidden(): static
    {
        return $this->assertStatus(Response::HTTP_FORBIDDEN);
    }

    #[\Override]
    public function assertNotFound(): static
    {
        return $this->assertStatus(Response::HTTP_NOT_FOUND);
    }

    #[\Override]
    public function assertUnprocessable(): static
    {
        return $this->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
    }

    #[\Override]
    public function assertMatchesExpectedBody(): static
    {
        return $this->assertExcluding([]);
    }

    /**
     * Compares actual vs expected body after stripping the builder blacklist
     * plus any additional runtime fields.
     *
     * @param list<string> $fields
     */
    #[\Override]
    public function assertExcluding(array $fields): static
    {
        Assert::assertNotNull(
            $this->expectedBody,
            'No expected body configured. Call withResponseExample() on the builder first.',
        );

        $exclude = \array_merge($this->blacklist, $fields);

        $expected = $this->expectedBody;
        $actual = $this->decodedBody;

        ArrayTool::removeKeyRecursive($expected, ...$exclude);
        ArrayTool::removeKeyRecursive($actual, ...$exclude);

        Assert::assertSame($expected, $actual);

        return $this;
    }

    /** @param array<mixed> $subset */
    #[\Override]
    public function assertJsonContains(array $subset): static
    {
        foreach ($subset as $key => $value) {
            Assert::assertArrayHasKey($key, $this->decodedBody);
            Assert::assertSame($value, $this->decodedBody[$key]);
        }

        return $this;
    }

    #[\Override]
    public function assertJsonHasKey(string $key): static
    {
        Assert::assertArrayHasKey($key, $this->decodedBody);

        return $this;
    }

    #[\Override]
    public function json(?string $key = null): mixed
    {
        if (null === $key) {
            return $this->decodedBody;
        }

        return $this->decodedBody[$key] ?? null;
    }

    #[\Override]
    public function content(): string
    {
        if ($this->response instanceof BinaryFileResponse) {
            return $this->response->getFile()->getContent();
        }

        $content = $this->response->getContent();

        if (false === $content) {
            return '';
        }

        return $content;
    }

    #[\Override]
    public function assertBinaryContent(string $expected): static
    {
        Assert::assertSame($expected, $this->content());

        return $this;
    }

    #[\Override]
    public function header(string $name): ?string
    {
        return $this->response->headers->get($name);
    }

    #[\Override]
    public function assertHeader(string $name, string $value): static
    {
        Assert::assertSame($value, $this->header($name));

        return $this;
    }

    #[\Override]
    public function assertHeaderContains(string $name, string $value): static
    {
        Assert::assertStringContainsString($value, $this->header($name) ?? '');

        return $this;
    }
}
