<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Tests\Integration;

use OCA\Mitgliederverwaltung\Controller\FeeController;
use OCA\Mitgliederverwaltung\Db\FeeRecord;
use OCA\Mitgliederverwaltung\Db\FeeRule;
use OCA\Mitgliederverwaltung\Service\FeeCalculationService;
use OCA\Mitgliederverwaltung\Service\ValidationException;
use OCA\Mitgliederverwaltung\Tests\TestInputStream;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\IRequest;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

/**
 * Integration-level tests for FeeController API endpoints.
 *
 * Part of Issue #65.
 */
class FeeControllerTest extends TestCase {

    private FeeController $controller;
    private FeeCalculationService&MockObject $feeService;
    private IRequest&MockObject $request;
    private LoggerInterface&MockObject $logger;

    protected function setUp(): void {
        parent::setUp();

        $this->feeService = $this->createMock(FeeCalculationService::class);
        $this->request = $this->createMock(IRequest::class);
        $this->logger = $this->createMock(LoggerInterface::class);

        $this->controller = new FeeController(
            'mitgliederverwaltung',
            $this->request,
            $this->feeService,
            $this->logger
        );
    }

    protected function tearDown(): void {
        TestInputStream::unregister();
        parent::tearDown();
    }

    private function setRequestBody(array $data): void {
        TestInputStream::register();
        TestInputStream::setData(json_encode($data));
    }

    private function makeFeeRule(array $overrides = []): FeeRule {
        $rule = new FeeRule();
        $rule->setId($overrides['id'] ?? 1);
        $rule->setYearFrom($overrides['yearFrom'] ?? 2025);
        $rule->setBaseRate($overrides['baseRate'] ?? '120.00');
        $rule->setFamilyRulesJson($overrides['familyRulesJson'] ?? '{}');
        $rule->setInactiveRule($overrides['inactiveRule'] ?? 'freeze_rate_at_exit');
        $rule->setCreatedAt($overrides['createdAt'] ?? '2025-01-01');
        $rule->setCreatedBy($overrides['createdBy'] ?? 'admin');
        return $rule;
    }

    private function makeFeeRecord(array $overrides = []): FeeRecord {
        $record = new FeeRecord();
        $record->setId($overrides['id'] ?? 1);
        $record->setMemberId($overrides['memberId'] ?? 1);
        $record->setYear($overrides['year'] ?? 2026);
        $record->setAmount($overrides['amount'] ?? '120.00');
        $record->setPaid($overrides['paid'] ?? false);
        $record->setPaymentDate($overrides['paymentDate'] ?? null);
        return $record;
    }

    // ── GET /api/v1/fees/rules ──────────────────────────────────────

    public function testListRulesReturns200(): void {
        $this->feeService->method('getAllRules')->willReturn([]);

        $response = $this->controller->listRules();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testListRulesReturns500OnException(): void {
        $this->feeService->method('getAllRules')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->listRules();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
        $this->assertArrayHasKey('error', $response->getData());
    }

    // ── POST /api/v1/fees/rules ─────────────────────────────────────

    public function testCreateRuleReturns201OnSuccess(): void {
        $ruleData = ['yearFrom' => 2026, 'baseRate' => '150.00'];
        $this->setRequestBody($ruleData);
        $this->feeService->method('createRule')->willReturn($this->makeFeeRule(['yearFrom' => 2026, 'baseRate' => '150.00']));

        $response = $this->controller->createRule();

        $this->assertSame(Http::STATUS_CREATED, $response->getStatus());
    }

    public function testCreateRuleReturns400OnValidationError(): void {
        $this->setRequestBody(['yearFrom' => null]);
        $this->feeService->method('createRule')
            ->willThrowException(new ValidationException('year_from ist erforderlich'));

        $response = $this->controller->createRule();

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
    }

    public function testCreateRuleReturns500OnException(): void {
        $this->setRequestBody(['yearFrom' => 2026]);
        $this->feeService->method('createRule')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->createRule();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── PUT /api/v1/fees/rules/{ruleId} ─────────────────────────────

    public function testUpdateRuleReturns200OnSuccess(): void {
        $this->setRequestBody(['baseRate' => '200.00']);
        $this->feeService->method('updateRule')
            ->willReturn($this->makeFeeRule(['baseRate' => '200.00']));

        $response = $this->controller->updateRule(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testUpdateRuleReturns404WhenNotFound(): void {
        $this->setRequestBody(['baseRate' => '200.00']);
        $this->feeService->method('updateRule')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->updateRule(999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testUpdateRuleReturns400OnValidationError(): void {
        $this->setRequestBody(['baseRate' => '-10']);
        $this->feeService->method('updateRule')
            ->willThrowException(new ValidationException('Invalid amount'));

        $response = $this->controller->updateRule(1);

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
    }

    public function testUpdateRuleReturns500OnException(): void {
        $this->setRequestBody(['baseRate' => '200.00']);
        $this->feeService->method('updateRule')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->updateRule(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── GET /api/v1/fees/records ────────────────────────────────────

    public function testListRecordsReturns400WhenYearMissing(): void {
        $this->request->method('getParam')->willReturn(null);

        $response = $this->controller->listRecords();

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
    }

    public function testListRecordsReturns200WithYear(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name) {
                return $name === 'year' ? '2026' : null;
            });
        $this->feeService->method('getFeeRecordsForYear')->willReturn([]);

        $response = $this->controller->listRecords();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testListRecordsReturns500OnException(): void {
        $this->request->method('getParam')
            ->willReturnCallback(function ($name) {
                return $name === 'year' ? '2026' : null;
            });
        $this->feeService->method('getFeeRecordsForYear')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->listRecords();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── GET /api/v1/fees/members/{memberId}/records ─────────────────

    public function testMemberRecordsReturns200(): void {
        $this->feeService->method('getMemberFeeHistory')->willReturn([]);

        $response = $this->controller->memberRecords(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testMemberRecordsReturns500OnException(): void {
        $this->feeService->method('getMemberFeeHistory')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->memberRecords(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── POST /api/v1/fees/batch-calculate ───────────────────────────

    public function testBatchCalculateReturns400WhenYearMissing(): void {
        // php://input returns empty, so year will be null
        $response = $this->controller->batchCalculate();

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
    }

    public function testBatchCalculateReturns200OnSuccess(): void {
        $this->setRequestBody(['year' => 2026]);
        $this->feeService->method('batchCalculate')
            ->willReturn(['calculated' => 10, 'skipped' => 0]);

        $response = $this->controller->batchCalculate();

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
        $data = $response->getData();
        $this->assertSame(10, $data['calculated']);
    }

    public function testBatchCalculateReturns404WhenNoRuleFound(): void {
        $this->setRequestBody(['year' => 1900]);
        $this->feeService->method('batchCalculate')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->batchCalculate();

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testBatchCalculateReturns500OnException(): void {
        $this->setRequestBody(['year' => 2026]);
        $this->feeService->method('batchCalculate')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->batchCalculate();

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── PUT /api/v1/fees/records/{recordId}/paid ────────────────────

    public function testMarkPaidReturns200OnSuccess(): void {
        $this->setRequestBody(['paymentDate' => '2026-04-01']);
        $this->feeService->method('markAsPaid')
            ->willReturn($this->makeFeeRecord(['paid' => true]));

        $response = $this->controller->markPaid(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testMarkPaidUsesDefaultDateWhenNotProvided(): void {
        $this->setRequestBody([]);
        $this->feeService->method('markAsPaid')
            ->willReturn($this->makeFeeRecord(['paid' => true]));

        $response = $this->controller->markPaid(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testMarkPaidReturns404WhenNotFound(): void {
        $this->setRequestBody(['paymentDate' => '2026-04-01']);
        $this->feeService->method('markAsPaid')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->markPaid(999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testMarkPaidReturns500OnException(): void {
        $this->setRequestBody(['paymentDate' => '2026-04-01']);
        $this->feeService->method('markAsPaid')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->markPaid(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    // ── PUT /api/v1/fees/records/{recordId}/override ────────────────

    public function testManualOverrideReturns200OnSuccess(): void {
        $this->setRequestBody(['amount' => '50.00', 'notes' => 'Reduced']);
        $this->feeService->method('manualOverride')
            ->willReturn($this->makeFeeRecord(['amount' => '50.00']));

        $response = $this->controller->manualOverride(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }

    public function testManualOverrideReturns400WhenAmountMissing(): void {
        $this->setRequestBody(['notes' => 'test']);

        $response = $this->controller->manualOverride(1);

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
    }

    public function testManualOverrideReturns400WhenBodyEmpty(): void {
        // No setRequestBody => empty php://input
        $response = $this->controller->manualOverride(1);

        $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
    }

    public function testManualOverrideReturns404WhenNotFound(): void {
        $this->setRequestBody(['amount' => '50.00']);
        $this->feeService->method('manualOverride')
            ->willThrowException(new DoesNotExistException(''));

        $response = $this->controller->manualOverride(999);

        $this->assertSame(Http::STATUS_NOT_FOUND, $response->getStatus());
    }

    public function testManualOverrideReturns500OnException(): void {
        $this->setRequestBody(['amount' => '50.00']);
        $this->feeService->method('manualOverride')
            ->willThrowException(new \RuntimeException('DB error'));

        $response = $this->controller->manualOverride(1);

        $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
    }

    public function testManualOverrideWithoutNotes(): void {
        $this->setRequestBody(['amount' => '50.00']);
        $this->feeService->method('manualOverride')
            ->willReturn($this->makeFeeRecord(['amount' => '50.00']));

        $response = $this->controller->manualOverride(1);

        $this->assertSame(Http::STATUS_OK, $response->getStatus());
    }
}
