<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\Injury;
use OCA\Mitgliederverwaltung\Db\InjuryMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\InjuryService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class InjuryServiceTest extends TestCase {

    private InjuryService $service;
    private InjuryMapper&MockObject $injuryMapper;
    private MemberMapper&MockObject $memberMapper;
    private IDBConnection&MockObject $db;
    private AuditService&MockObject $auditService;
    private IUserSession&MockObject $userSession;
    private LoggerInterface&MockObject $logger;

    protected function setUp(): void {
        $this->injuryMapper = $this->createMock(InjuryMapper::class);
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->db = $this->createMock(IDBConnection::class);
        $this->auditService = $this->createMock(AuditService::class);
        $this->userSession = $this->createMock(IUserSession::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $user = $this->createMock(IUser::class);
        $user->method('getUID')->willReturn('testuser');
        $this->userSession->method('getUser')->willReturn($user);

        $this->service = new InjuryService(
            $this->injuryMapper,
            $this->memberMapper,
            $this->db,
            $this->auditService,
            $this->userSession,
            $this->logger
        );
    }

    private function createQueryBuilderMock(): IQueryBuilder&MockObject {
        $qb = $this->createMock(IQueryBuilder::class);
        $expr = $this->createMock(IExpressionBuilder::class);
        $expr->method('eq')->willReturn('');
        $qb->method('expr')->willReturn($expr);
        $qb->method('insert')->willReturnSelf();
        $qb->method('delete')->willReturnSelf();
        $qb->method('select')->willReturnSelf();
        $qb->method('from')->willReturnSelf();
        $qb->method('where')->willReturnSelf();
        $qb->method('andWhere')->willReturnSelf();
        $qb->method('values')->willReturnSelf();
        $qb->method('createNamedParameter')->willReturn('?');
        $qb->method('executeStatement')->willReturn(1);

        $result = $this->createMock(\OCP\DB\IResult::class);
        $result->method('fetch')->willReturn(false);
        $result->method('closeCursor')->willReturn(true);
        $qb->method('executeQuery')->willReturn($result);

        return $qb;
    }

    private function createInjuryEntity(int $id, int $memberId): Injury {
        $injury = new Injury();
        $injury->setDatum('2024-07-10');
        $injury->setMemberId($memberId);
        $injury->setBeschreibung('Knie verletzt');
        $injury->setCreatedBy('testuser');
        $injury->setCreatedAt('2024-07-10 14:30:00');
        $ref = new \ReflectionProperty($injury, 'id');
        $ref->setAccessible(true);
        $ref->setValue($injury, $id);
        return $injury;
    }

    private function createMemberEntity(int $id): Member {
        $member = new Member();
        $member->setVorname('Max');
        $member->setNachname('Mueller');
        $member->setGeburtsdatum('2010-01-01');
        $member->setRolle('mitglied');
        $member->setEintritt('2020-01-01');
        $member->setStatus('aktiv');
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, $id);
        return $member;
    }

    // ── create() ───────────────────────────────────────────────────

    public function testCreateSucceeds(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('insert')->willReturn($injury);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->create([
            'datum' => '2024-07-10',
            'memberId' => 10,
            'beschreibung' => 'Knie verletzt',
        ]);

        $this->assertEquals('2024-07-10', $result['datum']);
        $this->assertEquals(10, $result['memberId']);
        $this->assertArrayHasKey('involvedMembers', $result);
        $this->assertArrayHasKey('memberVorname', $result);
        $this->assertEquals('Max', $result['memberVorname']);
    }

    public function testCreateWithInvolvedMembers(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('insert')->willReturn($injury);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->create([
            'datum' => '2024-07-10',
            'memberId' => 10,
            'beschreibung' => 'Knie verletzt',
        ], [20, 30]);

        // No exception means involved members were added
        $this->assertNotEmpty($result);
    }

    public function testCreateThrowsOnMissingDatum(): void {
        $this->expectException(ValidationException::class);

        $this->service->create([
            'memberId' => 10,
            'beschreibung' => 'Knie verletzt',
        ]);
    }

    public function testCreateThrowsOnMissingMemberId(): void {
        $this->expectException(ValidationException::class);

        $this->service->create([
            'datum' => '2024-07-10',
            'beschreibung' => 'Knie verletzt',
        ]);
    }

    public function testCreateThrowsOnMissingBeschreibung(): void {
        $this->expectException(ValidationException::class);

        $this->service->create([
            'datum' => '2024-07-10',
            'memberId' => 10,
        ]);
    }

    public function testCreateThrowsOnEmptyBeschreibung(): void {
        $this->expectException(ValidationException::class);

        $this->service->create([
            'datum' => '2024-07-10',
            'memberId' => 10,
            'beschreibung' => '  ',
        ]);
    }

    // ── find() ─────────────────────────────────────────────────────

    public function testFindReturnsInjuryWithDetails(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findById')->with(1)->willReturn($injury);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->find(1);

        $this->assertEquals(1, $result['id']);
        $this->assertEquals('Max', $result['memberVorname']);
    }

    public function testFindThrowsOnNotFound(): void {
        $this->injuryMapper->method('findById')
            ->willThrowException(new DoesNotExistException(''));

        $this->expectException(DoesNotExistException::class);
        $this->service->find(999);
    }

    // ── findAll() ──────────────────────────────────────────────────

    public function testFindAllReturnsAllInjuries(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury1 = $this->createInjuryEntity(1, 10);
        $injury2 = $this->createInjuryEntity(2, 20);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findAll')->willReturn([$injury1, $injury2]);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->findAll();

        $this->assertCount(2, $result);
    }

    public function testFindAllFiltersByMember(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findByMemberId')->with(10)->willReturn([$injury]);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->findAll(memberId: 10);

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

    public function testFindAllFiltersByLager(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findByLagerId')->with(5)->willReturn([$injury]);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->findAll(lagerId: 5);

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

    public function testFindAllFiltersByDateRange(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findByDateRange')
            ->with('2024-01-01', '2024-12-31')
            ->willReturn([$injury]);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->findAll(dateFrom: '2024-01-01', dateTo: '2024-12-31');

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

    // ── getInjuriesForMember() ─────────────────────────────────────

    public function testGetInjuriesForMember(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findByMemberId')->with(10)->willReturn([$injury]);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->getInjuriesForMember(10);

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

    // ── getInjuriesForLager() ──────────────────────────────────────

    public function testGetInjuriesForLager(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findByLagerId')->with(5)->willReturn([$injury]);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->getInjuriesForLager(5);

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

    // ── getInjuriesForDateRange() ──────────────────────────────────

    public function testGetInjuriesForDateRange(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findByDateRange')
            ->with('2024-01-01', '2024-06-30')
            ->willReturn([$injury]);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->getInjuriesForDateRange('2024-01-01', '2024-06-30');

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

    // ── update() ───────────────────────────────────────────────────

    public function testUpdateSucceeds(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $updatedInjury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findById')->with(1)->willReturn($injury);
        $this->injuryMapper->method('update')->willReturn($updatedInjury);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->update(1, ['beschreibung' => 'Arm verletzt']);

        $this->assertNotEmpty($result);
    }

    public function testUpdateWithInvolvedMembers(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findById')->with(1)->willReturn($injury);
        $this->injuryMapper->method('update')->willReturn($injury);
        $this->memberMapper->method('findById')->willReturn($member);

        $result = $this->service->update(1, ['beschreibung' => 'Test'], [20, 30]);

        $this->assertNotEmpty($result);
    }

    // ── delete() ───────────────────────────────────────────────────

    public function testDeleteSucceeds(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 10);
        $this->injuryMapper->method('findById')->with(1)->willReturn($injury);
        $this->injuryMapper->expects($this->once())->method('delete');

        $this->service->delete(1);
    }

    public function testDeleteThrowsOnNotFound(): void {
        $this->injuryMapper->method('findById')
            ->willThrowException(new DoesNotExistException(''));

        $this->expectException(DoesNotExistException::class);
        $this->service->delete(999);
    }

    // ── addInvolvedMember() / removeInvolvedMember() ───────────────

    public function testAddInvolvedMember(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        // Should not throw
        $this->service->addInvolvedMember(1, 10);
        $this->assertTrue(true);
    }

    public function testRemoveInvolvedMember(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        // Should not throw
        $this->service->removeInvolvedMember(1, 10);
        $this->assertTrue(true);
    }

    // ── buildResponse resolves member/lager names ──────────────────

    public function testBuildResponseResolvesLagerName(): void {
        // The buildResponse method calls getQueryBuilder() twice:
        // once for the lager name, once for involved members.
        // Use willReturnCallback to differentiate.
        $callCount = 0;
        $this->db->method('getQueryBuilder')->willReturnCallback(function () use (&$callCount) {
            $callCount++;
            $qb = $this->createMock(IQueryBuilder::class);
            $expr = $this->createMock(\OCP\DB\QueryBuilder\IExpressionBuilder::class);
            $expr->method('eq')->willReturn('');
            $qb->method('expr')->willReturn($expr);
            $qb->method('select')->willReturnSelf();
            $qb->method('from')->willReturnSelf();
            $qb->method('where')->willReturnSelf();
            $qb->method('createNamedParameter')->willReturn('?');

            $result = $this->createMock(\OCP\DB\IResult::class);
            $result->method('closeCursor')->willReturn(true);

            if ($callCount === 1) {
                // First call: lager name lookup
                $result->method('fetch')->willReturn(['name' => 'Sommerlager']);
            } else {
                // Second call: involved members
                $result->method('fetch')->willReturn(false);
            }

            $qb->method('executeQuery')->willReturn($result);
            return $qb;
        });

        $injury = $this->createInjuryEntity(1, 10);
        $injury->setLagerId(5);
        $member = $this->createMemberEntity(10);

        $this->injuryMapper->method('findById')->with(1)->willReturn($injury);
        $this->memberMapper->method('findById')->willReturn($member);

        $resultData = $this->service->find(1);

        $this->assertEquals('Sommerlager', $resultData['lagerName']);
    }

    public function testBuildResponseHandlesMissingMember(): void {
        $qb = $this->createQueryBuilderMock();
        $this->db->method('getQueryBuilder')->willReturn($qb);

        $injury = $this->createInjuryEntity(1, 999);

        $this->injuryMapper->method('findById')->willReturn($injury);
        $this->memberMapper->method('findById')
            ->willThrowException(new DoesNotExistException(''));

        $result = $this->service->find(1);

        $this->assertEquals('Unbekannt', $result['memberVorname']);
    }
}
