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

use PHPUnit\Framework\TestCase;
use Domain\User\UserRepository;

class UserRepositoryTest extends TestCase
{
    public function testFindReturnsUserWhenExists(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_users', '*', ['id' => 7])
            ->willReturn(['id' => 7, 'login' => 'admin']);

        $repository = new UserRepository($mockDb);
        $user = $repository->find(7);

        $this->assertIsArray($user);
        $this->assertSame(7, (int)$user['id']);
        $this->assertSame('admin', $user['login']);
    }

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

        $repository = new UserRepository($mockDb);
        $this->assertNull($repository->find(123));
    }

    public function testCheckLoginReturnsErrorWhenLoginIsTaken(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_users', 'login', [
                'AND' => [
                    'login' => 'taken@example.com',
                    'id[!]' => 10,
                ],
            ])
            ->willReturn('taken@example.com');

        $repository = new UserRepository($mockDb);
        $response = $repository->checkLogin('taken@example.com', 10);

        $this->assertSame('error', $response['status']);
    }

    public function testCheckLoginReturnsOkWhenAvailable(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn(null);

        $repository = new UserRepository($mockDb);
        $response = $repository->checkLogin('free@example.com', 5);

        $this->assertSame('ok', $response['status']);
    }

    public function testSaveReturnsErrorForTooShortPasswordOnCreate(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new UserRepository($mockDb);

        $response = $repository->save(0, 'admin@example.com', 'on', '1234', '1234', 1, 'on', 'admin@example.com');

        $this->assertSame('error', $response['status']);
    }

    public function testSaveReturnsErrorForMismatchedPasswordsOnCreate(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new UserRepository($mockDb);

        $response = $repository->save(0, 'admin@example.com', 'on', 'password1', 'password2', 1);

        $this->assertSame('error', $response['status']);
        $this->assertStringContainsString('rozne', $response['msg']);
    }

    public function testSaveCreatesUserWithNormalizedSwitches(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('insert')
            ->with('pp_users', $this->callback(function (array $row): bool {
                return $row['login'] === 'new@example.com'
                    && $row['status'] === 1
                    && $row['admin'] === 1
                    && $row['twofa_enabled'] === 1
                    && $row['twofa_email'] === '2fa@example.com'
                    && $row['password'] === md5('secret5');
            }))
            ->willReturn($this->createMock(\PDOStatement::class));

        $repository = new UserRepository($mockDb);
        $response = $repository->save(0, 'new@example.com', 'on', 'secret5', 'secret5', 1, 'on', '2fa@example.com');

        $this->assertSame('ok', $response['status']);
    }

    public function testSaveUpdatesExistingUserWithPassword(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->exactly(2))
            ->method('update')
            ->withConsecutive(
                [
                    'pp_users',
                    $this->callback(fn(array $d) => $d['password'] === md5('newpass5')),
                    ['id' => 5],
                ],
                [
                    'pp_users',
                    $this->callback(fn(array $d) => $d['login'] === 'user@example.com' && $d['status'] === 1),
                    ['id' => 5],
                ]
            );

        $repository = new UserRepository($mockDb);
        $response = $repository->save(5, 'user@example.com', 'on', 'newpass5', 'newpass5', 1);

        $this->assertSame('ok', $response['status']);
    }

    public function testSaveUpdatesExistingUserWithoutPassword(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_users',
                $this->callback(fn(array $d) => $d['login'] === 'user@example.com'),
                ['id' => 5]
            );

        $repository = new UserRepository($mockDb);
        $response = $repository->save(5, 'user@example.com', 'on', '', '', 1);

        $this->assertSame('ok', $response['status']);
    }

    public function testSaveReturnsErrorForTooShortPasswordOnUpdate(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new UserRepository($mockDb);

        $response = $repository->save(5, 'user@example.com', 1, '123', '123', 1);

        $this->assertSame('error', $response['status']);
    }

    public function testSaveReturnsErrorForMismatchedPasswordsOnUpdate(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $repository = new UserRepository($mockDb);

        $response = $repository->save(5, 'user@example.com', 1, 'password1', 'password2', 1);

        $this->assertSame('error', $response['status']);
    }

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

        $repository = new UserRepository($mockDb);
        $this->assertTrue($repository->delete(3));
    }

    public function testDeleteReturnsFalseOnFailure(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('delete')->willReturn(false);

        $repository = new UserRepository($mockDb);
        $this->assertFalse($repository->delete(999));
    }

    public function testDetailsReturnsUserByLogin(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_users', '*', ['login' => 'admin@shop.com'])
            ->willReturn(['id' => 1, 'login' => 'admin@shop.com']);

        $repository = new UserRepository($mockDb);
        $user = $repository->details('admin@shop.com');

        $this->assertIsArray($user);
        $this->assertSame('admin@shop.com', $user['login']);
    }

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

        $repository = new UserRepository($mockDb);
        $this->assertNull($repository->details('nonexistent@shop.com'));
    }

    public function testLogonReturnsSuccessForValidCredentials(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $mockDb->expects($this->exactly(3))
            ->method('get')
            ->withConsecutive(
                ['pp_users', '*', ['login' => 'admin@example.com']],
                ['pp_users', '*', ['AND' => ['login' => 'admin@example.com', 'status' => 1, 'error_logged_count[<]' => 5]]],
                ['pp_users', '*', ['AND' => ['login' => 'admin@example.com', 'status' => 1, 'password' => md5('password123')]]]
            )
            ->willReturnOnConsecutiveCalls(
                ['id' => 5],
                ['id' => 5],
                ['id' => 5]
            );

        $mockDb->expects($this->once())
            ->method('update')
            ->with(
                'pp_users',
                $this->callback(function (array $data): bool {
                    return isset($data['last_logged']) && $data['error_logged_count'] === 0;
                }),
                ['login' => 'admin@example.com']
            );

        $repository = new UserRepository($mockDb);
        $result = $repository->logon('admin@example.com', 'password123');

        $this->assertSame(1, $result);
    }

    public function testLogonReturnsZeroForNonexistentUser(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('get')
            ->with('pp_users', '*', ['login' => 'noone@example.com'])
            ->willReturn(false);

        $repository = new UserRepository($mockDb);
        $this->assertSame(0, $repository->logon('noone@example.com', 'pass'));
    }

    public function testLogonReturnsNegativeOneForBlockedUser(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->exactly(2))
            ->method('get')
            ->withConsecutive(
                ['pp_users', '*', ['login' => 'blocked@example.com']],
                ['pp_users', '*', ['AND' => ['login' => 'blocked@example.com', 'status' => 1, 'error_logged_count[<]' => 5]]]
            )
            ->willReturnOnConsecutiveCalls(
                ['id' => 3, 'status' => 0],
                false
            );

        $repository = new UserRepository($mockDb);
        $this->assertSame(-1, $repository->logon('blocked@example.com', 'pass'));
    }

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

        $repository = new UserRepository($mockDb);
        $this->assertFalse($repository->verifyTwofaCode(999, '123456'));
    }

    public function testVerifyTwofaCodeReturnsFalseAfterMaxAttempts(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'id' => 1,
            'twofa_failed_attempts' => 5,
            'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
            'twofa_expires_at' => date('Y-m-d H:i:s', time() + 300),
        ]);

        $repository = new UserRepository($mockDb);
        $this->assertFalse($repository->verifyTwofaCode(1, '123456'));
    }

    public function testVerifyTwofaCodeReturnsFalseForExpiredCode(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'id' => 1,
            'twofa_failed_attempts' => 0,
            'twofa_code_hash' => password_hash('123456', PASSWORD_DEFAULT),
            'twofa_expires_at' => date('Y-m-d H:i:s', time() - 60),
        ]);

        $mockDb->expects($this->once())->method('update');

        $repository = new UserRepository($mockDb);
        $this->assertFalse($repository->verifyTwofaCode(1, '123456'));
    }

    public function testVerifyTwofaCodeReturnsTrueForValidCode(): void
    {
        $code = '654321';
        $hash = password_hash($code, PASSWORD_DEFAULT);

        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'id' => 1,
            'twofa_failed_attempts' => 0,
            'twofa_code_hash' => $hash,
            'twofa_expires_at' => date('Y-m-d H:i:s', time() + 300),
        ]);

        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_users', $this->callback(function (array $d): bool {
                return $d['twofa_code_hash'] === null
                    && $d['twofa_expires_at'] === null
                    && $d['twofa_failed_attempts'] === 0;
            }), ['id' => 1]);

        $repository = new UserRepository($mockDb);
        $this->assertTrue($repository->verifyTwofaCode(1, $code));
    }

    public function testSendTwofaCodeReturnsFalseWhen2FADisabled(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'id' => 1,
            'twofa_enabled' => 0,
            'login' => 'test@example.com',
        ]);

        $repository = new UserRepository($mockDb);
        $this->assertFalse($repository->sendTwofaCode(1));
    }

    public function testSendTwofaCodeReturnsFalseForInvalidEmail(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->method('get')->willReturn([
            'id' => 1,
            'twofa_enabled' => 1,
            'twofa_email' => 'not-an-email',
            'login' => 'also-not-email',
        ]);

        $repository = new UserRepository($mockDb);
        $this->assertFalse($repository->sendTwofaCode(1));
    }

    public function testUpdateByIdCallsDbUpdate(): void
    {
        $mockDb = $this->createMock(\medoo::class);
        $mockDb->expects($this->once())
            ->method('update')
            ->with('pp_users', ['status' => 0], ['id' => 7])
            ->willReturn($this->createMock(\PDOStatement::class));

        $repository = new UserRepository($mockDb);
        $this->assertTrue($repository->updateById(7, ['status' => 0]));
    }

    public function testListForAdminReturnsItemsAndTotal(): void
    {
        $mockDb = $this->createMock(\medoo::class);

        $countStmt = $this->createMock(\PDOStatement::class);
        $countStmt->expects($this->once())
            ->method('fetchAll')
            ->willReturn([[2]]);

        $dataStmt = $this->createMock(\PDOStatement::class);
        $dataStmt->expects($this->once())
            ->method('fetchAll')
            ->willReturn([
                ['id' => 2, 'login' => 'a@example.com', 'status' => 1],
                ['id' => 3, 'login' => 'b@example.com', 'status' => 0],
            ]);

        $mockDb->expects($this->exactly(2))
            ->method('query')
            ->willReturnOnConsecutiveCalls($countStmt, $dataStmt);

        $repository = new UserRepository($mockDb);
        $result = $repository->listForAdmin(['login' => '', 'status' => ''], 'login', 'ASC', 1, 15);

        $this->assertSame(2, $result['total']);
        $this->assertCount(2, $result['items']);
        $this->assertSame('a@example.com', $result['items'][0]['login']);
    }
}
