<?php

declare(strict_types=1);

namespace NoahVet\Reef\Test\A_Unit\Domain\Validation\V2;

use NoahVet\Reef\Domain\Validation\V2\OpenApiPropertyValidator;
use NoahVet\Reef\Domain\Validation\V2\OpenApiValidator;
use NoahVet\Reef\Domain\Validation\V2\OpenApiValidatorInterface;
use PHPUnit\Framework\TestCase;

/**
 * This class respects the pattern:
 * testGiven<Context>When<Method>Then<ExpectedResult>
 */
final class OpenApiValidatorTest extends TestCase
{
    private OpenApiValidatorInterface $subject;

    protected function setUp(): void
    {
        $this->subject = new OpenApiValidator([
            new OpenApiPropertyValidator(),
        ]);
    }

    /**
     * Given a deeply nested payload and matching validation rules including all OpenAPI formats,
     * When validate call,
     * Then all property validators are recursively applied and validation returns true.
     */
    public function testGivenNestedPayloadWithAllOpenApiFormatsWhenValidateCallThenReturnsTrue(): void
    {
        $payload = [
            'clinic' => [
                'id' => '550e8400-e29b-41d4-a716-446655440000',
                'name' => 'Happy Paws',
                'email' => 'contact@happypaws.test',
                'isActive' => true,
                'rating' => 4.7,
                'foundedYear' => 2012,
                'openedAt' => '2024-01-01T09:00:00Z',
                'openingDate' => '2024-01-01',
                'website' => 'https://www.happypaws.test',
                'hostname' => 'api.happypaws.test',
                'ipv4' => '192.168.1.10',
                'ipv6' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
                'adminPassword' => 'SuperSecretPassword!',
                'logoBinary' => 'binary-content',
                'licenseFile' => \base64_encode('PDF_CONTENT'),
                'description' => null,
                'address' => [
                    'streetName' => 'Vet Street',
                    'city' => 'Paris',
                    'zipCode' => '75000',
                ],
                'tags' => ['emergency', 'cats', 'dogs'],
            ],
            'patients' => [
                [
                    'id' => 1,
                    'name' => 'Rex',
                    'species' => 'dog',
                    'age' => 5,
                    'weight' => 32.5,
                    'vaccinated' => true,
                    'medicalNotes' => null,
                ],
            ],
        ];

        $rules = [
            'clinic' => [
                'type' => 'object',
                'required' => ['id', 'name', 'email', 'isActive'],
                'properties' => [
                    'id' => [
                        'type' => 'string',
                        'format' => 'uuid',
                    ],
                    'name' => [
                        'type' => 'string',
                        'minLength' => 3,
                    ],
                    'email' => [
                        'type' => 'string',
                        'format' => 'email',
                    ],
                    'isActive' => [
                        'type' => 'boolean',
                    ],
                    'rating' => [
                        'type' => 'number',
                    ],
                    'foundedYear' => [
                        'type' => 'integer',
                    ],
                    'openedAt' => [
                        'type' => 'string',
                        'format' => 'date-time',
                    ],
                    'openingDate' => [
                        'type' => 'string',
                        'format' => 'date',
                    ],
                    'website' => [
                        'type' => 'string',
                        'format' => 'uri',
                    ],
                    'hostname' => [
                        'type' => 'string',
                        'format' => 'hostname',
                    ],
                    'ipv4' => [
                        'type' => 'string',
                        'format' => 'ipv4',
                    ],
                    'ipv6' => [
                        'type' => 'string',
                        'format' => 'ipv6',
                    ],
                    'adminPassword' => [
                        'type' => 'string',
                        'format' => 'password',
                    ],
                    'licenseFile' => [
                        'type' => 'string',
                        'format' => 'byte',
                    ],
                    'logoBinary' => [
                        'type' => 'string',
                        'format' => 'binary',
                    ],
                    'description' => [
                        'type' => 'string',
                        'nullable' => true,
                    ],
                    'address' => [
                        'type' => 'object',
                        'properties' => [
                            'streetName' => [
                                'type' => 'string',
                            ],
                            'city' => [
                                'type' => 'string',
                            ],
                            'zipCode' => [
                                'type' => 'string',
                            ],
                        ],
                    ],
                    'tags' => [
                        'type' => 'array',
                        'items' => [
                            'type' => 'string',
                        ],
                    ],
                ],
            ],
            'patients' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'id' => [
                            'type' => 'integer',
                        ],
                        'name' => [
                            'type' => 'string',
                        ],
                        'species' => [
                            'type' => 'string',
                            'enum' => ['dog', 'cat', 'bird'],
                        ],
                        'age' => [
                            'type' => 'integer',
                        ],
                        'weight' => [
                            'type' => 'number',
                        ],
                        'vaccinated' => [
                            'type' => 'boolean',
                        ],
                        'medicalNotes' => [
                            'type' => 'string',
                            'nullable' => true,
                        ],
                    ],
                ],
            ],
        ];

        $this->assertTrue(
            $this->subject->validate($payload, $rules),
        );
    }

    /**
     * Given an array of integers with an invalid string item,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenIntegerArrayWithInvalidItemWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'ids' => [1, 2, 'oops', 4],
        ];

        $rules = [
            'ids' => [
                'type' => 'array',
                'items' => [
                    'type' => 'integer',
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given an array of nullable integers containing a null value,
     * When validating,
     * Then validation returns true.
     */
    public function testGivenNullableIntegerArrayWithNullItemWhenValidatingThenReturnsTrue(): void
    {
        $payload = [
            'ids' => [1, null, 3],
        ];

        $rules = [
            'ids' => [
                'type' => 'array',
                'items' => [
                    'type' => 'integer',
                    'nullable' => true,
                ],
            ],
        ];

        $this->assertTrue($this->subject->validate($payload, $rules));
    }

    /**
     * Given a field with an enum constraint and a payload value within the enum,
     * When validating,
     * Then validation returns true.
     */
    public function testGivenEnumConstraintAndValidPayloadValueWhenValidatingThenReturnsTrue(): void
    {
        $payload = [
            'species' => 'dog',
        ];

        $rules = [
            'species' => [
                'type' => 'string',
                'enum' => ['dog', 'cat', 'bird'],
            ],
        ];

        $this->assertTrue($this->subject->validate($payload, $rules));
    }

    /**
     * Given a field with an enum constraint and a payload value outside the enum,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenEnumConstraintAndInvalidPayloadValueWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'species' => 'lizard',
        ];

        $rules = [
            'species' => [
                'type' => 'string',
                'enum' => ['dog', 'cat', 'bird'],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given an array of enum strings within the allowed set,
     * When validating,
     * Then validation returns true.
     */
    public function testGivenEnumArrayItemsAndValidValuesWhenValidatingThenReturnsTrue(): void
    {
        $payload = [
            'species' => ['dog', 'cat'],
        ];

        $rules = [
            'species' => [
                'type' => 'array',
                'items' => [
                    'type' => 'string',
                    'enum' => ['dog', 'cat', 'bird'],
                ],
            ],
        ];

        $this->assertTrue($this->subject->validate($payload, $rules));
    }

    /**
     * Given an array of enum strings with one value outside the allowed set,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenEnumArrayItemsAndInvalidValueWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'species' => ['dog', 'lizard'],
        ];

        $rules = [
            'species' => [
                'type' => 'array',
                'items' => [
                    'type' => 'string',
                    'enum' => ['dog', 'cat', 'bird'],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given a nested object with an invalid child property,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenNestedObjectWithInvalidChildWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'clinic' => [
                'address' => [
                    'street' => 123,
                ],
            ],
        ];

        $rules = [
            'clinic' => [
                'type' => 'object',
                'properties' => [
                    'address' => [
                        'type' => 'object',
                        'properties' => [
                            'street' => [
                                'type' => 'string',
                            ],
                        ],
                    ],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given an array of objects with one invalid item,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenArrayOfObjectsWithInvalidItemWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'products' => [
                [
                    'name' => 'valid',
                ],
                'invalid-item',
            ],
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given an associative payload for array items with required fields satisfied,
     * When validating,
     * Then validation returns true.
     */
    public function testGivenAssociativeArrayItemsWithRequiredFieldsWhenValidatingThenReturnsTrue(): void
    {
        $payload = [
            'products' => [
                'name' => 'valid',
            ],
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                    'required' => ['name'],
                ],
            ],
        ];

        $this->assertTrue($this->subject->validate($payload, $rules));
    }

    /**
     * Given a list of array items with a missing required field on one item,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenArrayItemsWithMissingRequiredOnOneItemWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'products' => [
                [
                    'name' => 'valid',
                ],
                [
                    // missing name
                ],
            ],
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                    'required' => ['name'],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given a list payload with an invalid required field type on one item,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenArrayItemsWithInvalidRequiredFieldTypeWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'products' => [
                [
                    'name' => 'valid',
                ],
                [
                    'name' => 123, // invalid type
                ],
            ],
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                    'required' => ['name'],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given a non-list associative payload for array items with missing required field,
     * When validating,
     * Then validation returns false.
     */
    public function testGivenAssociativeArrayItemsMissingRequiredWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'products' => [
                'sku' => '123', // missing required name
            ],
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                    'required' => ['name'],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given an associative payload for array items missing the required key,
     * When validating,
     * Then validation returns false at the required-check step.
     */
    public function testGivenAssociativeArrayItemsWithoutRequiredFieldWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'products' => [
                'description' => 'no required name',
            ],
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                    'required' => ['name'],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given a list payload where one array item is missing the required field,
     * When validating,
     * Then validation returns false during the per-item required check.
     */
    public function testGivenListPayloadWithItemMissingRequiredFieldWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'products' => [
                [
                    'name' => 'valid',
                ],
                [
                    // missing name
                ],
            ],
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                    'required' => ['name'],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given a nested object where the child payload is not an array but has required properties,
     * When validating,
     * Then validation returns false after casting the child payload to an empty array.
     */
    public function testGivenNonArrayChildPayloadWithRequiredPropertiesWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'clinic' => [
                'address' => 'not-an-array',
            ],
        ];

        $rules = [
            'clinic' => [
                'type' => 'object',
                'properties' => [
                    'address' => [
                        'type' => 'object',
                        'properties' => [
                            'street' => [
                                'type' => 'string',
                            ],
                        ],
                        'required' => ['street'],
                    ],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }

    /**
     * Given an array property defined with required items but a non-array payload value,
     * When validating,
     * Then validation returns false because required item structure is missing.
     */
    public function testGivenNonArrayPayloadForArrayWithRequiredItemsWhenValidatingThenReturnsFalse(): void
    {
        $payload = [
            'products' => 'not-an-array',
        ];

        $rules = [
            'products' => [
                'type' => 'array',
                'items' => [
                    'type' => 'object',
                    'properties' => [
                        'name' => [
                            'type' => 'string',
                        ],
                    ],
                    'required' => ['name'],
                ],
            ],
        ];

        $this->assertFalse($this->subject->validate($payload, $rules));
    }
}
