<?php
namespace Tests\Unit\api\Controllers;

use PHPUnit\Framework\TestCase;
use api\Controllers\ProductsApiController;
use Domain\Attribute\AttributeRepository;
use Domain\Product\ProductRepository;

class ProductsApiControllerTest extends TestCase
{
    private $mockRepo;
    private $mockAttrRepo;
    private $controller;

    protected function setUp(): void
    {
        $this->mockRepo = $this->createMock(ProductRepository::class);
        $this->mockAttrRepo = $this->createMock(AttributeRepository::class);
        $this->controller = new ProductsApiController($this->mockRepo, $this->mockAttrRepo);

        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET = [];
    }

    protected function tearDown(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET = [];
        http_response_code(200);
    }

    // --- list ---

    public function testListReturnsProducts(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';

        $this->mockRepo->method('listForApi')
            ->willReturn([
                'items' => [
                    ['id' => 1, 'name' => 'Product A', 'price_brutto' => 99.99, 'status' => 1],
                ],
                'total' => 1,
                'page' => 1,
                'per_page' => 50,
            ]);

        ob_start();
        $this->controller->list();
        $output = ob_get_clean();

        $json = json_decode($output, true);
        $this->assertSame('ok', $json['status']);
        $this->assertCount(1, $json['data']['items']);
        $this->assertSame(1, $json['data']['total']);
    }

    public function testListRejectsPostMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'POST';

        ob_start();
        $this->controller->list();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    public function testListPassesFiltersToRepository(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['search'] = 'test';
        $_GET['status'] = '1';
        $_GET['promoted'] = '0';
        $_GET['sort'] = 'price_brutto';
        $_GET['sort_dir'] = 'ASC';
        $_GET['page'] = '2';
        $_GET['per_page'] = '25';

        $this->mockRepo->expects($this->once())
            ->method('listForApi')
            ->with(
                $this->callback(function ($filters) {
                    return $filters['search'] === 'test'
                        && $filters['status'] === '1'
                        && $filters['promoted'] === '0';
                }),
                'price_brutto',
                'ASC',
                2,
                25
            )
            ->willReturn(['items' => [], 'total' => 0, 'page' => 2, 'per_page' => 25]);

        ob_start();
        $this->controller->list();
        ob_get_clean();
    }

    public function testListDefaultPagination(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';

        $this->mockRepo->expects($this->once())
            ->method('listForApi')
            ->with(
                $this->anything(),
                'id',
                'DESC',
                1,
                50
            )
            ->willReturn(['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 50]);

        ob_start();
        $this->controller->list();
        ob_get_clean();
    }

    public function testListClampsPerPageTo100(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['per_page'] = '200';

        $this->mockRepo->expects($this->once())
            ->method('listForApi')
            ->with(
                $this->anything(),
                $this->anything(),
                $this->anything(),
                $this->anything(),
                100
            )
            ->willReturn(['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 100]);

        ob_start();
        $this->controller->list();
        ob_get_clean();
    }

    // --- get ---

    public function testGetReturnsProduct(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '42';

        $this->mockRepo->method('findForApi')
            ->with(42)
            ->willReturn([
                'id' => 42,
                'name' => 'Test Product',
                'price_brutto' => 199.99,
                'status' => 1,
                'languages' => [],
                'images' => [],
                'categories' => [],
                'attributes' => [],
            ]);

        ob_start();
        $this->controller->get();
        $output = ob_get_clean();

        $json = json_decode($output, true);
        $this->assertSame('ok', $json['status']);
        $this->assertSame(42, $json['data']['id']);
    }

    public function testGetReturns404WhenProductNotFound(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '999';

        $this->mockRepo->method('findForApi')
            ->with(999)
            ->willReturn(null);

        ob_start();
        $this->controller->get();
        $output = ob_get_clean();

        $this->assertSame(404, http_response_code());
        $json = json_decode($output, true);
        $this->assertSame('NOT_FOUND', $json['code']);
    }

    public function testGetReturns400WhenMissingId(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';

        ob_start();
        $this->controller->get();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    public function testGetRejectsPostMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'POST';
        $_GET['id'] = '1';

        ob_start();
        $this->controller->get();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    // --- create ---

    public function testCreateRejectsGetMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';

        ob_start();
        $this->controller->create();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    public function testCreateReturns400WhenNoBody(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'POST';

        // php://input returns empty in test environment → getJsonBody() returns null
        ob_start();
        $this->controller->create();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
        $json = json_decode($output, true);
        $this->assertSame('BAD_REQUEST', $json['code']);
    }

    // --- update ---

    public function testUpdateRejectsGetMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '1';

        ob_start();
        $this->controller->update();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    public function testUpdateReturns400WhenMissingId(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'PUT';

        ob_start();
        $this->controller->update();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    public function testUpdateReturns404WhenProductNotFound(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'PUT';
        $_GET['id'] = '999';

        $this->mockRepo->method('find')
            ->with(999)
            ->willReturn(null);

        ob_start();
        $this->controller->update();
        $output = ob_get_clean();

        $this->assertSame(404, http_response_code());
        $json = json_decode($output, true);
        $this->assertSame('NOT_FOUND', $json['code']);
    }

    public function testUpdateReturns400WhenNoBody(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'PUT';
        $_GET['id'] = '1';

        $this->mockRepo->method('find')
            ->with(1)
            ->willReturn(['id' => 1, 'status' => 1, 'promoted' => 0]);

        // php://input returns empty in test environment → getJsonBody() returns null
        ob_start();
        $this->controller->update();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
        $json = json_decode($output, true);
        $this->assertSame('BAD_REQUEST', $json['code']);
    }

    // --- mapApiToFormData (tested indirectly via reflection) ---

    public function testMapApiToFormDataConvertsStatusToCheckbox(): void
    {
        $method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
        $method->setAccessible(true);

        $result = $method->invoke($this->controller, ['status' => 1, 'promoted' => 0]);

        $this->assertSame('on', $result['status']);
        $this->assertSame('', $result['promoted']);
    }

    public function testMapApiToFormDataMapsLanguages(): void
    {
        $method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
        $method->setAccessible(true);

        $body = [
            'languages' => [
                'pl' => ['name' => 'Nazwa PL', 'description' => 'Opis PL'],
                'en' => ['name' => 'Name EN'],
            ],
        ];

        $result = $method->invoke($this->controller, $body);

        $this->assertSame('Nazwa PL', $result['name']['pl']);
        $this->assertSame('Opis PL', $result['description']['pl']);
        $this->assertSame('Name EN', $result['name']['en']);
    }

    public function testMapApiToFormDataMapsNumericFields(): void
    {
        $method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
        $method->setAccessible(true);

        $body = [
            'price_brutto' => 99.99,
            'vat' => 23,
            'quantity' => 10,
            'sku' => 'PROD-001',
            'ean' => '5901234123457',
        ];

        $result = $method->invoke($this->controller, $body);

        $this->assertSame(99.99, $result['price_brutto']);
        $this->assertSame(23, $result['vat']);
        $this->assertSame(10, $result['quantity']);
        $this->assertSame('PROD-001', $result['sku']);
        $this->assertSame('5901234123457', $result['ean']);
    }

    public function testMapApiToFormDataPreservesZeroBasePriceForSaveProduct(): void
    {
        $method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
        $method->setAccessible(true);

        $result = $method->invoke($this->controller, [
            'price_brutto' => 0.0,
            'languages' => ['pl' => ['name' => 'Zero']],
        ]);

        $this->assertSame('0', $result['price_brutto']);
    }

    public function testMapApiToFormDataMapsCategories(): void
    {
        $method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
        $method->setAccessible(true);

        $body = [
            'categories' => [1, 5, 12],
        ];

        $result = $method->invoke($this->controller, $body);

        $this->assertSame([1, 5, 12], $result['categories']);
    }

    public function testMapApiToFormDataPartialUpdatePreservesExisting(): void
    {
        $method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
        $method->setAccessible(true);

        $existing = [
            'status' => 1,
            'promoted' => 0,
            'price_brutto' => 50.00,
            'vat' => 23,
            'quantity' => 5,
            'sku' => 'OLD-SKU',
        ];

        // Only update price
        $body = ['price_brutto' => 75.00];

        $result = $method->invoke($this->controller, $body, $existing);

        $this->assertSame(75.00, $result['price_brutto']);
        $this->assertSame('on', $result['status']); // preserved from existing
        $this->assertSame('', $result['promoted']); // preserved from existing (0 → '')
        $this->assertSame(23, $result['vat']); // preserved
        $this->assertSame('OLD-SKU', $result['sku']); // preserved
    }

    public function testMapApiToFormDataMapsForeignKeys(): void
    {
        $method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
        $method->setAccessible(true);

        $body = [
            'set_id' => 3,
            'producer_id' => 7,
            'product_unit_id' => 2,
        ];

        $result = $method->invoke($this->controller, $body);

        $this->assertSame(3, $result['set']);
        $this->assertSame(7, $result['producer_id']);
        $this->assertSame(2, $result['product_unit']);
    }

    // --- variants ---

    public function testVariantsReturnsVariantsList(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '1';

        $this->mockRepo->method('find')
            ->with(1)
            ->willReturn(['id' => 1, 'parent_id' => null]);

        $this->mockRepo->method('findVariantsForApi')
            ->with(1)
            ->willReturn([
                [
                    'id' => 101,
                    'permutation_hash' => '5-12',
                    'sku' => 'SKU-M',
                    'attributes' => [['attribute_id' => 5, 'value_id' => 12]],
                ],
            ]);

        $this->mockAttrRepo->method('listForApi')
            ->willReturn([
                ['id' => 5, 'type' => 0, 'status' => 1, 'names' => ['pl' => 'Rozmiar'], 'values' => []],
            ]);

        ob_start();
        $this->controller->variants();
        $output = ob_get_clean();

        $json = json_decode($output, true);
        $this->assertSame('ok', $json['status']);
        $this->assertSame(1, $json['data']['product_id']);
        $this->assertCount(1, $json['data']['variants']);
        $this->assertCount(1, $json['data']['available_attributes']);
    }

    public function testVariantsReturns400WhenMissingId(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';

        ob_start();
        $this->controller->variants();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    public function testVariantsReturns404WhenProductNotFound(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '999';

        $this->mockRepo->method('find')->willReturn(null);

        ob_start();
        $this->controller->variants();
        $output = ob_get_clean();

        $this->assertSame(404, http_response_code());
    }

    public function testVariantsReturns400ForVariantProduct(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '101';

        $this->mockRepo->method('find')
            ->with(101)
            ->willReturn(['id' => 101, 'parent_id' => 1]);

        ob_start();
        $this->controller->variants();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    public function testVariantsRejectsPostMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'POST';
        $_GET['id'] = '1';

        ob_start();
        $this->controller->variants();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    // --- create_variant ---

    public function testCreateVariantRejectsGetMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '1';

        ob_start();
        $this->controller->create_variant();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    public function testCreateVariantReturns400WhenMissingId(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'POST';

        ob_start();
        $this->controller->create_variant();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    public function testCreateVariantReturns400WhenNoBody(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'POST';
        $_GET['id'] = '1';

        ob_start();
        $this->controller->create_variant();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    // --- update_variant ---

    public function testUpdateVariantRejectsGetMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '101';

        ob_start();
        $this->controller->update_variant();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    public function testUpdateVariantReturns400WhenMissingId(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'PUT';

        ob_start();
        $this->controller->update_variant();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    public function testUpdateVariantReturns400WhenNoBody(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'PUT';
        $_GET['id'] = '101';

        ob_start();
        $this->controller->update_variant();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    // --- delete_variant ---

    public function testDeleteVariantRejectsGetMethod(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['id'] = '101';

        ob_start();
        $this->controller->delete_variant();
        $output = ob_get_clean();

        $this->assertSame(405, http_response_code());
    }

    public function testDeleteVariantReturns400WhenMissingId(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'DELETE';

        ob_start();
        $this->controller->delete_variant();
        $output = ob_get_clean();

        $this->assertSame(400, http_response_code());
    }

    public function testDeleteVariantReturns404WhenNotFound(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'DELETE';
        $_GET['id'] = '999';

        $this->mockRepo->method('deleteVariantForApi')
            ->with(999)
            ->willReturn(false);

        ob_start();
        $this->controller->delete_variant();
        $output = ob_get_clean();

        $this->assertSame(404, http_response_code());
    }

    public function testDeleteVariantSuccess(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'DELETE';
        $_GET['id'] = '101';

        $this->mockRepo->method('deleteVariantForApi')
            ->with(101)
            ->willReturn(true);

        ob_start();
        $this->controller->delete_variant();
        $output = ob_get_clean();

        $json = json_decode($output, true);
        $this->assertSame('ok', $json['status']);
        $this->assertSame(101, $json['data']['id']);
        $this->assertTrue($json['data']['deleted']);
    }

    // --- list with attribute filter ---

    public function testListPassesAttributeFilters(): void
    {
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_GET['attribute_5'] = '12';
        $_GET['attribute_7'] = '18';

        $this->mockRepo->expects($this->once())
            ->method('listForApi')
            ->with(
                $this->callback(function ($filters) {
                    return isset($filters['attributes'])
                        && $filters['attributes'][5] === 12
                        && $filters['attributes'][7] === 18;
                }),
                $this->anything(),
                $this->anything(),
                $this->anything(),
                $this->anything()
            )
            ->willReturn(['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 50]);

        ob_start();
        $this->controller->list();
        ob_get_clean();
    }
}
