<?php
namespace Tests\Unit\Domain\Client;

use PHPUnit\Framework\TestCase;
use Domain\Client\ClientRepository;

class ClientRepositoryTest extends TestCase
{
    public function testListForAdminWhitelistsSortAndPagination(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $queries = [];

        $mockDb->method('query')
            ->willReturnCallback(function ($sql, $params = []) use (&$queries) {
                $queries[] = ['sql' => $sql, 'params' => $params];

                if (strpos($sql, 'COUNT(0)') !== false) {
                    return new class {
                        public function fetchAll() { return [[1]]; }
                    };
                }

                return new class {
                    public function fetchAll() {
                        return [[
                            'client_id' => 5,
                            'client_name' => 'Jan',
                            'client_surname' => 'Kowalski',
                            'client_email' => 'jan@example.com',
                            'client_phone' => '123',
                            'client_city' => 'Warszawa',
                            'total_orders' => 3,
                            'total_spent' => 199.99,
                            'is_registered' => 1,
                        ]];
                    }
                };
            });

        $repository = new ClientRepository($mockDb);
        $result = $repository->listForAdmin(
            [],
            'client_name DESC; DROP TABLE pp_shop_orders; --',
            'DESC; DELETE FROM pp_users; --',
            1,
            999
        );

        $this->assertCount(2, $queries);
        $dataSql = $queries[1]['sql'];

        $this->assertMatchesRegularExpression('/ORDER BY\s+c\.client_surname\s+ASC,\s+c\.client_surname\s+ASC,\s+c\.client_name\s+ASC/i', $dataSql);
        $this->assertStringNotContainsString('DROP TABLE', $dataSql);
        $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);

        $this->assertSame(1, $result['total']);
        $this->assertCount(1, $result['items']);
        $this->assertSame('Jan', $result['items'][0]['client_name']);
    }

    public function testOrdersForClientReturnsEmptyOnMissingInput(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('query');

        $repository = new ClientRepository($mockDb);

        $this->assertSame([], $repository->ordersForClient('', 'Kowalski', 'jan@example.com'));
        $this->assertSame([], $repository->ordersForClient('Jan', '', 'jan@example.com'));
        $this->assertSame([], $repository->ordersForClient('Jan', 'Kowalski', ''));
    }

    public function testOrdersForClientNormalizesRows(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('query')
            ->willReturn(new class {
                public function fetchAll() {
                    return [[
                        'id' => '10',
                        'date_order' => '2026-02-15 10:00:00',
                        'summary' => '149.50',
                        'payment_method' => 'Przelew',
                        'transport' => 'Kurier',
                        'message' => null,
                    ]];
                }
            });

        $repository = new ClientRepository($mockDb);
        $rows = $repository->ordersForClient('Jan', 'Kowalski', 'jan@example.com');

        $this->assertCount(1, $rows);
        $this->assertSame(10, $rows[0]['id']);
        $this->assertSame(149.50, $rows[0]['summary']);
        $this->assertSame('Przelew', $rows[0]['payment_method']);
        $this->assertSame('', $rows[0]['message']);
    }

    public function testTotalsForClientReturnsZeroForMissingInput(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('query');

        $repository = new ClientRepository($mockDb);
        $totals = $repository->totalsForClient('', 'Kowalski', 'jan@example.com');

        $this->assertSame(0, $totals['total_orders']);
        $this->assertSame(0.0, $totals['total_spent']);
    }

    public function testTotalsForClientReturnsAggregatedValues(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('query')
            ->willReturn(new class {
                public function fetchAll() {
                    return [[
                        'total_orders' => '4',
                        'total_spent' => '456.78',
                    ]];
                }
            });

        $repository = new ClientRepository($mockDb);
        $totals = $repository->totalsForClient('Jan', 'Kowalski', 'jan@example.com');

        $this->assertSame(4, $totals['total_orders']);
        $this->assertSame(456.78, $totals['total_spent']);
    }

    // ===== Frontend methods =====

    public function testClientDetailsReturnsNullForInvalidId(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('get');

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->clientDetails(0));
        $this->assertNull($repo->clientDetails(-1));
    }

    public function testClientDetailsReturnsRowOnSuccess(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_shop_clients', '*', ['id' => 5])
            ->willReturn(['id' => 5, 'email' => 'jan@example.com']);

        $repo = new ClientRepository($mockDb);
        $result = $repo->clientDetails(5);
        $this->assertSame('jan@example.com', $result['email']);
    }

    public function testClientDetailsReturnsNullWhenNotFound(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn(false);

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->clientDetails(999));
    }

    public function testClientEmailReturnsNullForInvalidId(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('get');

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->clientEmail(0));
    }

    public function testClientEmailReturnsStringOnSuccess(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_shop_clients', 'email', ['id' => 3])
            ->willReturn('test@example.com');

        $repo = new ClientRepository($mockDb);
        $this->assertSame('test@example.com', $repo->clientEmail(3));
    }

    public function testClientEmailReturnsNullWhenNotFound(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn(false);

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->clientEmail(999));
    }

    public function testClientAddressesReturnsEmptyForInvalidId(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('select');

        $repo = new ClientRepository($mockDb);
        $this->assertSame([], $repo->clientAddresses(0));
    }

    public function testClientAddressesReturnsRows(): void
    {
        $rows = [
            ['id' => 1, 'client_id' => 5, 'city' => 'Warszawa'],
            ['id' => 2, 'client_id' => 5, 'city' => 'Kraków'],
        ];

        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('select')
            ->with('pp_shop_clients_addresses', '*', ['client_id' => 5])
            ->willReturn($rows);

        $repo = new ClientRepository($mockDb);
        $this->assertCount(2, $repo->clientAddresses(5));
    }

    public function testClientAddressesHandlesFalseFromDb(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('select')
            ->willReturn(false);

        $repo = new ClientRepository($mockDb);
        $this->assertSame([], $repo->clientAddresses(1));
    }

    public function testAddressDetailsReturnsNullForInvalidId(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('get');

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->addressDetails(0));
    }

    public function testAddressDetailsReturnsRow(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_shop_clients_addresses', '*', ['id' => 7])
            ->willReturn(['id' => 7, 'city' => 'Gdańsk']);

        $repo = new ClientRepository($mockDb);
        $result = $repo->addressDetails(7);
        $this->assertSame('Gdańsk', $result['city']);
    }

    public function testAddressDeleteReturnsFalseForInvalidId(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('delete');

        $repo = new ClientRepository($mockDb);
        $this->assertFalse($repo->addressDelete(0));
    }

    public function testAddressDeleteReturnsTrueOnSuccess(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('delete')
            ->with('pp_shop_clients_addresses', ['id' => 3])
            ->willReturn(1);

        $repo = new ClientRepository($mockDb);
        $this->assertTrue($repo->addressDelete(3));
    }

    public function testAddressSaveReturnsFalseForInvalidClientId(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('insert');
        $mockDb->expects($this->never())->method('update');

        $repo = new ClientRepository($mockDb);
        $this->assertFalse($repo->addressSave(0, null, ['name' => 'Jan']));
    }

    public function testAddressSaveInsertsNewAddress(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('insert')
            ->with(
                'pp_shop_clients_addresses',
                $this->callback(function ($row) {
                    return $row['client_id'] === 5
                        && $row['name'] === 'Jan'
                        && $row['city'] === 'Warszawa';
                })
            )
            ->willReturn(1);

        $repo = new ClientRepository($mockDb);
        $result = $repo->addressSave(5, null, [
            'name' => 'Jan',
            'surname' => 'Kowalski',
            'street' => 'Marszałkowska 1',
            'postal_code' => '00-001',
            'city' => 'Warszawa',
            'phone' => '123456789',
        ]);
        $this->assertTrue($result);
    }

    public function testAddressSaveUpdatesExistingAddress(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_shop_clients_addresses',
                $this->callback(function ($row) {
                    return $row['name'] === 'Anna' && !isset($row['client_id']);
                }),
                ['AND' => ['client_id' => 5, 'id' => 10]]
            )
            ->willReturn(1);

        $repo = new ClientRepository($mockDb);
        $result = $repo->addressSave(5, 10, [
            'name' => 'Anna',
            'surname' => 'Nowak',
            'street' => 'Piłsudskiego 2',
            'postal_code' => '30-001',
            'city' => 'Kraków',
            'phone' => '987654321',
        ]);
        $this->assertTrue($result);
    }

    public function testMarkAddressAsCurrentReturnsFalseForInvalidIds(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('update');

        $repo = new ClientRepository($mockDb);
        $this->assertFalse($repo->markAddressAsCurrent(0, 1));
        $this->assertFalse($repo->markAddressAsCurrent(1, 0));
    }

    public function testMarkAddressAsCurrentResetsAndSets(): void
    {
        $calls = [];
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->exactly(2))
            ->method('update')
            ->willReturnCallback(function ($table, $data, $where) use (&$calls) {
                $calls[] = ['data' => $data, 'where' => $where];
                return 1;
            });

        $repo = new ClientRepository($mockDb);
        $this->assertTrue($repo->markAddressAsCurrent(5, 3));

        $this->assertSame(['current' => 0], $calls[0]['data']);
        $this->assertSame(['client_id' => 5], $calls[0]['where']);
        $this->assertSame(['current' => 1], $calls[1]['data']);
        $this->assertSame(['AND' => ['client_id' => 5, 'id' => 3]], $calls[1]['where']);
    }

    public function testAuthenticateReturnsErrorOnEmptyInput(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('get');

        $repo = new ClientRepository($mockDb);

        $result = $repo->authenticate('', 'pass');
        $this->assertSame('error', $result['status']);

        $result = $repo->authenticate('jan@example.com', '');
        $this->assertSame('error', $result['status']);
    }

    public function testAuthenticateReturnsErrorWhenClientNotFound(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn(false);

        $repo = new ClientRepository($mockDb);
        $result = $repo->authenticate('nobody@example.com', 'pass');
        $this->assertSame('error', $result['status']);
        $this->assertSame('logowanie-nieudane', $result['code']);
    }

    public function testAuthenticateReturnsInactiveForUnconfirmedAccount(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn([
                'id' => 1,
                'password' => md5('2026-01-01 00:00:00' . 'test123'),
                'register_date' => '2026-01-01 00:00:00',
                'hash' => 'abc123',
                'status' => 0,
            ]);

        $repo = new ClientRepository($mockDb);
        $result = $repo->authenticate('jan@example.com', 'test123');
        $this->assertSame('inactive', $result['status']);
        $this->assertSame('abc123', $result['hash']);
    }

    public function testAuthenticateReturnsErrorOnWrongPassword(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn([
                'id' => 1,
                'password' => md5('2026-01-01 00:00:00' . 'correct'),
                'register_date' => '2026-01-01 00:00:00',
                'hash' => 'abc',
                'status' => 1,
            ]);

        $repo = new ClientRepository($mockDb);
        $result = $repo->authenticate('jan@example.com', 'wrong');
        $this->assertSame('error', $result['status']);
        $this->assertSame('logowanie-blad-nieprawidlowe-haslo', $result['code']);
    }

    public function testAuthenticateReturnsOkOnSuccess(): void
    {
        $registerDate = '2026-01-01 00:00:00';
        $password = 'test123';

        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->exactly(2))
            ->method('get')
            ->willReturnOnConsecutiveCalls(
                [
                    'id' => 5,
                    'password' => md5($registerDate . $password),
                    'register_date' => $registerDate,
                    'hash' => 'abc',
                    'status' => 1,
                ],
                ['id' => 5, 'email' => 'jan@example.com', 'name' => 'Jan']
            );

        $repo = new ClientRepository($mockDb);
        $result = $repo->authenticate('jan@example.com', $password);
        $this->assertSame('ok', $result['status']);
        $this->assertSame(5, $result['client']['id']);
    }

    public function testCreateClientReturnsNullOnEmptyInput(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('count');

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->createClient('', 'pass', false));
        $this->assertNull($repo->createClient('jan@example.com', '', false));
    }

    public function testCreateClientReturnsNullWhenEmailTaken(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('count')
            ->with('pp_shop_clients', ['email' => 'jan@example.com'])
            ->willReturn(1);

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->createClient('jan@example.com', 'pass', false));
    }

    public function testCreateClientReturnsIdAndHashOnSuccess(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())->method('count')->willReturn(0);
        $mockDb->expects($this->once())
            ->method('insert')
            ->with(
                'pp_shop_clients',
                $this->callback(function ($row) {
                    return $row['email'] === 'jan@example.com'
                        && $row['agremment_marketing'] === 1
                        && !empty($row['hash'])
                        && !empty($row['password']);
                })
            )
            ->willReturn(1);
        $mockDb->expects($this->once())->method('id')->willReturn(42);

        $repo = new ClientRepository($mockDb);
        $result = $repo->createClient('jan@example.com', 'pass', true);
        $this->assertSame(42, $result['id']);
        $this->assertNotEmpty($result['hash']);
    }

    public function testConfirmRegistrationReturnsNullOnEmptyHash(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('get');

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->confirmRegistration(''));
    }

    public function testConfirmRegistrationReturnsNullWhenNotFound(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn(false);

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->confirmRegistration('nonexistent'));
    }

    public function testConfirmRegistrationActivatesAndReturnsEmail(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->exactly(2))
            ->method('get')
            ->willReturnOnConsecutiveCalls(10, 'jan@example.com');
        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_shop_clients', ['status' => 1], ['id' => 10]);

        $repo = new ClientRepository($mockDb);
        $this->assertSame('jan@example.com', $repo->confirmRegistration('validhash'));
    }

    public function testGenerateNewPasswordReturnsNullOnEmptyHash(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('get');

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->generateNewPassword(''));
    }

    public function testGenerateNewPasswordReturnsNullWhenNotFound(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn(false);

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->generateNewPassword('badhash'));
    }

    public function testGenerateNewPasswordReturnsEmailAndPassword(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn([
                'id' => 5,
                'email' => 'jan@example.com',
                'register_date' => '2026-01-01 00:00:00',
            ]);
        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_shop_clients',
                $this->callback(function ($data) {
                    return $data['password_recovery'] === 0
                        && !empty($data['password']);
                }),
                ['id' => 5]
            );

        $repo = new ClientRepository($mockDb);
        $result = $repo->generateNewPassword('validhash');
        $this->assertSame('jan@example.com', $result['email']);
        $this->assertSame(10, strlen($result['password']));
    }

    public function testInitiatePasswordRecoveryReturnsNullOnEmptyEmail(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('get');

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->initiatePasswordRecovery(''));
    }

    public function testInitiatePasswordRecoveryReturnsNullWhenNotFound(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn(false);

        $repo = new ClientRepository($mockDb);
        $this->assertNull($repo->initiatePasswordRecovery('nobody@example.com'));
    }

    public function testInitiatePasswordRecoverySetsRecoveryFlagAndReturnsHash(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->willReturn('abc123hash');
        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_shop_clients', ['password_recovery' => 1], ['email' => 'jan@example.com']);

        $repo = new ClientRepository($mockDb);
        $this->assertSame('abc123hash', $repo->initiatePasswordRecovery('jan@example.com'));
    }

    public function testClientOrdersReturnsEmptyForInvalidId(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->never())->method('select');

        $repo = new ClientRepository($mockDb);
        $this->assertSame([], $repo->clientOrders(0));
    }
}
