<?php

declare(strict_types=1);

namespace Grav\Plugin\Api\Tests\Unit;

use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;

/**
 * Static helper methods for building lightweight stubs used across the test suite.
 *
 * All request stubs are anonymous class implementations of PSR interfaces.
 * Config/Grav/User stubs come from our test Stubs/GravStubs.php and are
 * real instances of the Grav types (either the genuine classes or our
 * minimal stubs depending on the environment).
 */
final class TestHelper
{
    /**
     * Create a stub PSR-7 ServerRequest.
     */
    public static function createMockRequest(
        string $method = 'GET',
        string $path = '/',
        array $headers = [],
        string $body = '',
        array $queryParams = [],
        array $serverParams = [],
        array $attributes = [],
    ): ServerRequestInterface {
        return new class ($method, $path, $headers, $body, $queryParams, $serverParams, $attributes) implements ServerRequestInterface {
            public function __construct(
                private readonly string $method,
                private readonly string $path,
                private readonly array $headers,
                private readonly string $body,
                private readonly array $queryParams,
                private readonly array $serverParams,
                private array $attributes,
            ) {}

            public function getMethod(): string { return $this->method; }

            public function getUri(): UriInterface
            {
                $path = $this->path;
                return new class ($path) implements UriInterface {
                    public function __construct(private readonly string $path) {}
                    public function getScheme(): string { return 'https'; }
                    public function getAuthority(): string { return ''; }
                    public function getUserInfo(): string { return ''; }
                    public function getHost(): string { return 'localhost'; }
                    public function getPort(): ?int { return null; }
                    public function getPath(): string { return $this->path; }
                    public function getQuery(): string { return ''; }
                    public function getFragment(): string { return ''; }
                    public function withScheme(string $scheme): static { return clone $this; }
                    public function withUserInfo(string $user, ?string $password = null): static { return clone $this; }
                    public function withHost(string $host): static { return clone $this; }
                    public function withPort(?int $port): static { return clone $this; }
                    public function withPath(string $path): static { return clone $this; }
                    public function withQuery(string $query): static { return clone $this; }
                    public function withFragment(string $fragment): static { return clone $this; }
                    public function __toString(): string { return $this->path; }
                };
            }

            public function getBody(): StreamInterface
            {
                $body = $this->body;
                return new class ($body) implements StreamInterface {
                    public function __construct(private readonly string $content) {}
                    public function __toString(): string { return $this->content; }
                    public function close(): void {}
                    public function detach() { return null; }
                    public function getSize(): ?int { return strlen($this->content); }
                    public function tell(): int { return 0; }
                    public function eof(): bool { return true; }
                    public function isSeekable(): bool { return false; }
                    public function seek(int $offset, int $whence = SEEK_SET): void {}
                    public function rewind(): void {}
                    public function isWritable(): bool { return false; }
                    public function write(string $string): int { return 0; }
                    public function isReadable(): bool { return true; }
                    public function read(int $length): string { return $this->content; }
                    public function getContents(): string { return $this->content; }
                    public function getMetadata(?string $key = null): mixed { return null; }
                };
            }

            public function getQueryParams(): array { return $this->queryParams; }
            public function getServerParams(): array { return $this->serverParams; }

            public function getHeaderLine(string $name): string
            {
                foreach ($this->headers as $key => $value) {
                    if (strcasecmp($key, $name) === 0) {
                        return $value;
                    }
                }
                return '';
            }

            public function getHeader(string $name): array
            {
                foreach ($this->headers as $key => $value) {
                    if (strcasecmp($key, $name) === 0) {
                        return [$value];
                    }
                }
                return [];
            }

            public function hasHeader(string $name): bool
            {
                foreach ($this->headers as $key => $value) {
                    if (strcasecmp($key, $name) === 0) {
                        return true;
                    }
                }
                return false;
            }

            public function getHeaders(): array { return $this->headers; }

            public function getAttribute(string $name, mixed $default = null): mixed
            {
                return $this->attributes[$name] ?? $default;
            }

            public function withAttribute(string $name, mixed $value): static
            {
                $clone = clone $this;
                $clone->attributes[$name] = $value;
                return $clone;
            }

            public function getRequestTarget(): string { return $this->path; }
            public function withRequestTarget(string $requestTarget): static { return clone $this; }
            public function withMethod(string $method): static { return clone $this; }
            public function withUri(UriInterface $uri, bool $preserveHost = false): static { return clone $this; }
            public function getProtocolVersion(): string { return '1.1'; }
            public function withProtocolVersion(string $version): static { return clone $this; }
            public function withHeader(string $name, $value): static { return clone $this; }
            public function withAddedHeader(string $name, $value): static { return clone $this; }
            public function withoutHeader(string $name): static { return clone $this; }
            public function withBody(StreamInterface $body): static { return clone $this; }
            public function getCookieParams(): array { return []; }
            public function withCookieParams(array $cookies): static { return clone $this; }
            public function withQueryParams(array $query): static { return clone $this; }
            public function getUploadedFiles(): array { return []; }
            public function withUploadedFiles(array $uploadedFiles): static { return clone $this; }
            public function getParsedBody(): mixed { return null; }
            public function withParsedBody($data): static { return clone $this; }
            public function getAttributes(): array { return $this->attributes; }
            public function withoutAttribute(string $name): static {
                $clone = clone $this;
                unset($clone->attributes[$name]);
                return $clone;
            }
        };
    }

    /**
     * Create a Config instance from a nested data array.
     *
     * Returns a real Grav\Common\Config\Config (or our stub equivalent).
     */
    public static function createMockConfig(array $data = []): Config
    {
        return new Config($data);
    }

    /**
     * Create a mock user that duck-types Grav\Common\User\Interfaces\UserInterface.
     *
     * The returned object has a public $username property and supports
     * get(), set(), save(), and exists().
     */
    public static function createMockUser(
        string $username = 'testuser',
        array $data = [],
        bool $exists = true,
        ?object $blueprint = null,
    ): UserInterface {
        return new class ($username, $data, $exists, $blueprint) implements UserInterface {
            public readonly string $username;

            public function __construct(
                string $username,
                private array $data,
                private readonly bool $existsFlag,
                private readonly ?object $blueprint = null,
            ) {
                $this->username = $username;
            }

            public function get(string $key, mixed $default = null): mixed
            {
                return $this->data[$key] ?? $default;
            }

            public function set(string $key, mixed $value): void
            {
                $this->data[$key] = $value;
            }

            public function save(): void
            {
                // no-op in tests
            }

            public function exists(): bool
            {
                return $this->existsFlag;
            }

            public function getAvatarImage(): ?object
            {
                return null;
            }

            /**
             * Classic (DataUser) accounts expose the account blueprint via
             * blueprints(); UsersController::accountBlueprint() reads it to
             * apply and validate custom account fields (admin2#138). Returns
             * null unless a test injects one, keeping every other test's mock
             * blueprint-less exactly as before.
             */
            public function blueprints(): ?object
            {
                return $this->blueprint;
            }
        };
    }

    /**
     * Create a Grav container instance with given services.
     *
     * Returns the Grav singleton (reset between calls). A minimal 'config'
     * service that answers every get() with the caller's default is always
     * present unless the test supplies its own, so plugin code may read
     * config anywhere without the container blowing up mid-test.
     */
    public static function createMockGrav(array $services = []): Grav
    {
        Grav::resetInstance();
        $grav = Grav::instance();
        if (!array_key_exists('config', $services)) {
            $services['config'] = new class {
                public function get(string $key, mixed $default = null): mixed
                {
                    return $default;
                }
            };
        }
        foreach ($services as $key => $value) {
            $grav[$key] = $value;
        }
        return $grav;
    }

    /**
     * Create a mock accounts collection that is iterable and supports load().
     */
    public static function createMockAccounts(array $users = []): object
    {
        return new class ($users) implements \IteratorAggregate {
            /** @param array<string, object> $users keyed by username */
            public function __construct(private readonly array $users) {}

            public function getIterator(): \ArrayIterator
            {
                return new \ArrayIterator($this->users);
            }

            public function load(string $username): object
            {
                return $this->users[$username] ?? self::nonExistentUser($username);
            }

            private static function nonExistentUser(string $username): object
            {
                return new class ($username) {
                    public readonly string $username;
                    public function __construct(string $username)
                    {
                        $this->username = $username;
                    }
                    public function exists(): bool { return false; }
                    public function get(string $key, mixed $default = null): mixed { return $default; }
                };
            }
        };
    }
}
