<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Unit;

use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Db\Stufe;
use OCA\Mitgliederverwaltung\Db\StufeHistory;
use OCA\Mitgliederverwaltung\Db\StufeHistoryMapper;
use OCA\Mitgliederverwaltung\Db\StufeMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCA\Mitgliederverwaltung\Service\StufeService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class StufeServiceTest extends TestCase {

    private StufeService $service;
    private StufeMapper&MockObject $stufeMapper;
    private StufeHistoryMapper&MockObject $historyMapper;
    private MemberMapper&MockObject $memberMapper;
    private AuditService&MockObject $auditService;
    private IUserSession&MockObject $userSession;

    protected function setUp(): void {
        $this->stufeMapper = $this->createMock(StufeMapper::class);
        $this->historyMapper = $this->createMock(StufeHistoryMapper::class);
        $this->memberMapper = $this->createMock(MemberMapper::class);
        $this->auditService = $this->createMock(AuditService::class);
        $this->userSession = $this->createMock(IUserSession::class);

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

        $this->service = new StufeService(
            $this->stufeMapper,
            $this->historyMapper,
            $this->memberMapper,
            $this->auditService,
            $this->userSession
        );
    }

    private function createStufeEntity(int $id, string $name, int $order = 0): Stufe {
        $stufe = new Stufe();
        $stufe->setName($name);
        $stufe->setSortOrder($order);
        $ref = new \ReflectionProperty($stufe, 'id');
        $ref->setAccessible(true);
        $ref->setValue($stufe, $id);
        return $stufe;
    }

    private function createMemberEntity(int $id, string $status = 'aktiv', ?int $stufeId = null): 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($status);
        if ($stufeId !== null) {
            $member->setStufeId($stufeId);
        }
        $ref = new \ReflectionProperty($member, 'id');
        $ref->setAccessible(true);
        $ref->setValue($member, $id);
        return $member;
    }

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

    public function testFindAllReturnsStufen(): void {
        $stufe1 = $this->createStufeEntity(1, 'Woelflinge');
        $stufe2 = $this->createStufeEntity(2, 'Jungpfadfinder');

        $this->stufeMapper->method('findAll')->willReturn([$stufe1, $stufe2]);

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

        $this->assertCount(2, $result);
        $this->assertInstanceOf(Stufe::class, $result[0]);
    }

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

    public function testFindReturnsStufe(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $this->stufeMapper->method('findById')->with(1)->willReturn($stufe);

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

        $this->assertEquals('Woelflinge', $result->getName());
    }

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

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

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

    public function testCreateSucceeds(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $this->stufeMapper->method('insert')->willReturn($stufe);

        $result = $this->service->create(['name' => 'Woelflinge']);

        $this->assertEquals('Woelflinge', $result->getName());
    }

    public function testCreateWithAllFields(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $stufe->setSortOrder(1);
        $stufe->setAgeRangeMin(7);
        $stufe->setAgeRangeMax(10);
        $stufe->setColor('#ff0000');

        $this->stufeMapper->method('insert')->willReturn($stufe);

        $result = $this->service->create([
            'name' => 'Woelflinge',
            'sortOrder' => 1,
            'ageRangeMin' => 7,
            'ageRangeMax' => 10,
            'color' => '#ff0000',
        ]);

        $this->assertEquals(1, $result->getSortOrder());
    }

    public function testCreateThrowsOnEmptyName(): void {
        $this->expectException(ValidationException::class);
        $this->service->create(['name' => '']);
    }

    public function testCreateThrowsOnMissingName(): void {
        $this->expectException(ValidationException::class);
        $this->service->create([]);
    }

    public function testCreateThrowsOnWhitespaceName(): void {
        $this->expectException(ValidationException::class);
        $this->service->create(['name' => '   ']);
    }

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

    public function testUpdateSucceeds(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $this->stufeMapper->method('findById')->with(1)->willReturn($stufe);

        $updated = $this->createStufeEntity(1, 'Jungpfadfinder');
        $this->stufeMapper->method('update')->willReturn($updated);

        $result = $this->service->update(1, ['name' => 'Jungpfadfinder']);

        $this->assertEquals('Jungpfadfinder', $result->getName());
    }

    public function testUpdateThrowsOnEmptyName(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $this->stufeMapper->method('findById')->willReturn($stufe);

        $this->expectException(ValidationException::class);
        $this->service->update(1, ['name' => '']);
    }

    public function testUpdatePartialFields(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $stufe->setSortOrder(1);
        $this->stufeMapper->method('findById')->willReturn($stufe);
        $this->stufeMapper->method('update')->willReturn($stufe);

        $result = $this->service->update(1, ['sortOrder' => 5]);

        // The service should have updated sortOrder but kept name
        $this->assertNotNull($result);
    }

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

    public function testDeleteSucceeds(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $this->stufeMapper->method('findById')->willReturn($stufe);
        $this->memberMapper->method('findByStufe')->willReturn([]);

        $this->stufeMapper->expects($this->once())->method('delete');

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

    public function testDeleteThrowsWhenMembersAssigned(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge');
        $member = $this->createMemberEntity(1, 'aktiv', 1);

        $this->stufeMapper->method('findById')->willReturn($stufe);
        $this->memberMapper->method('findByStufe')->willReturn([$member]);

        $this->expectException(ValidationException::class);
        $this->expectExceptionMessage('Mitglieder zugewiesen');
        $this->service->delete(1);
    }

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

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

    // ── assignMemberToStufe() ──────────────────────────────────────

    public function testAssignMemberToStufe(): void {
        $member = $this->createMemberEntity(1, 'aktiv');
        $stufe = $this->createStufeEntity(5, 'Jungpfadfinder');

        $this->memberMapper->method('findById')->with(1)->willReturn($member);
        $this->stufeMapper->method('findById')->with(5)->willReturn($stufe);
        $this->memberMapper->expects($this->once())->method('update');
        $this->historyMapper->expects($this->once())->method('insert');

        $this->service->assignMemberToStufe(1, 5);
    }

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

        $this->expectException(DoesNotExistException::class);
        $this->service->assignMemberToStufe(999, 1);
    }

    public function testAssignMemberToStufeThrowsOnMissingStufe(): void {
        $member = $this->createMemberEntity(1);
        $this->memberMapper->method('findById')->willReturn($member);
        $this->stufeMapper->method('findById')
            ->willThrowException(new DoesNotExistException(''));

        $this->expectException(DoesNotExistException::class);
        $this->service->assignMemberToStufe(1, 999);
    }

    // ── getMemberStufeHistory() ────────────────────────────────────

    public function testGetMemberStufeHistory(): void {
        $history = new StufeHistory();
        $history->setMemberId(1);
        $history->setStufeId(5);
        $history->setChangedAt('2024-01-15 10:00:00');
        $history->setChangedBy('testuser');

        $this->historyMapper->method('findByMemberId')->with(1)->willReturn([$history]);

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

        $this->assertCount(1, $result);
        $this->assertInstanceOf(StufeHistory::class, $result[0]);
    }

    public function testGetMemberStufeHistoryEmpty(): void {
        $this->historyMapper->method('findByMemberId')->willReturn([]);

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

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

    // ── getSuggestedChanges() ──────────────────────────────────────

    public function testGetSuggestedChangesFindsOveragedMembers(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge', 1);
        $stufe->setAgeRangeMax(10);

        $nextStufe = $this->createStufeEntity(2, 'Jungpfadfinder', 2);
        $nextStufe->setAgeRangeMax(13);

        // Member is 12 years old, exceeds Woelflinge max of 10
        $member = $this->createMemberEntity(1, 'aktiv', 1);
        $member->setGeburtsdatum((new \DateTime())->modify('-12 years')->format('Y-m-d'));

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe, $nextStufe]);

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

        $this->assertCount(1, $result);
        $this->assertEquals(1, $result[0]['memberId']);
        $this->assertEquals('Woelflinge', $result[0]['currentStufe']['name']);
        $this->assertEquals('Jungpfadfinder', $result[0]['suggestedStufe']['name']);
        $this->assertEquals(2, $result[0]['exceededBy']);
    }

    public function testGetSuggestedChangesIgnoresInactiveMembers(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge', 1);
        $stufe->setAgeRangeMax(10);

        $member = $this->createMemberEntity(1, 'inaktiv', 1);
        $member->setGeburtsdatum((new \DateTime())->modify('-12 years')->format('Y-m-d'));

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

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

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

    public function testGetSuggestedChangesIgnoresMembersWithinRange(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge', 1);
        $stufe->setAgeRangeMax(10);

        $member = $this->createMemberEntity(1, 'aktiv', 1);
        $member->setGeburtsdatum((new \DateTime())->modify('-9 years')->format('Y-m-d'));

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

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

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

    public function testGetSuggestedChangesIgnoresMembersWithNoStufe(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge', 1);
        $stufe->setAgeRangeMax(10);

        $member = $this->createMemberEntity(1, 'aktiv');
        // No stufeId set
        $member->setGeburtsdatum((new \DateTime())->modify('-12 years')->format('Y-m-d'));

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

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

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

    public function testGetSuggestedChangesSuggestsNullWhenNoNextStufe(): void {
        $stufe = $this->createStufeEntity(1, 'Rover', 1);
        $stufe->setAgeRangeMax(16);

        $member = $this->createMemberEntity(1, 'aktiv', 1);
        $member->setGeburtsdatum((new \DateTime())->modify('-18 years')->format('Y-m-d'));

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

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

        $this->assertCount(1, $result);
        $this->assertNull($result[0]['suggestedStufe']);
    }

    public function testGetSuggestedChangesSortsByExceededByDescending(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge', 1);
        $stufe->setAgeRangeMax(10);

        $member1 = $this->createMemberEntity(1, 'aktiv', 1);
        $member1->setGeburtsdatum((new \DateTime())->modify('-12 years')->format('Y-m-d'));

        $member2 = $this->createMemberEntity(2, 'aktiv', 1);
        $member2->setVorname('Anna');
        $member2->setGeburtsdatum((new \DateTime())->modify('-15 years')->format('Y-m-d'));

        $this->memberMapper->method('findAll')->willReturn([$member1, $member2]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

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

        $this->assertCount(2, $result);
        // member2 has exceeded by 5, member1 by 2 - so member2 first
        $this->assertGreaterThan($result[1]['exceededBy'], $result[0]['exceededBy']);
    }

    // ── getSuggestionCount() ───────────────────────────────────────

    public function testGetSuggestionCountReturnsCount(): void {
        $stufe = $this->createStufeEntity(1, 'Woelflinge', 1);
        $stufe->setAgeRangeMax(10);

        $member = $this->createMemberEntity(1, 'aktiv', 1);
        $member->setGeburtsdatum((new \DateTime())->modify('-12 years')->format('Y-m-d'));

        $this->memberMapper->method('findAll')->willReturn([$member]);
        $this->stufeMapper->method('findAll')->willReturn([$stufe]);

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

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

    public function testGetSuggestionCountZero(): void {
        $this->memberMapper->method('findAll')->willReturn([]);
        $this->stufeMapper->method('findAll')->willReturn([]);

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

        $this->assertEquals(0, $result);
    }
}
