<?php

namespace Tests\Feature;

use App\Enums\AccrualFrequency;
use App\Enums\InvestmentStatus;
use App\Enums\InvestmentTransactionType;
use App\Mail\ConfirmInvestmentMail;
use App\Models\Investment;
use App\Models\InvestmentDuration;
use App\Models\InvestmentSetting;
use App\Models\InvestmentTransaction;
use App\Models\User;
use App\Support\InvestmentLedger;
use App\Support\UserInvestmentPortfolio;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class InvestmentTest extends TestCase
{
    use RefreshDatabase;

    public function test_investment_page_is_displayed(): void
    {
        $user = User::factory()->create();

        $response = $this
            ->actingAs($user)
            ->get(route('investments.index'));

        $response->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('investments/index')
                ->where('reinvest_mode', false));
    }

    public function test_investment_page_sets_reinvest_mode_from_query(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)
            ->get(route('investments.index', ['reinvertir' => '1']))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('investments/index')
                ->where('reinvest_mode', true));
    }

    public function test_mis_inversiones_redirects_guests(): void
    {
        $this->get(route('investments.mine'))
            ->assertRedirect(route('login'));
    }

    public function test_mis_inversiones_page_is_displayed_for_authenticated_user(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)
            ->get(route('investments.mine'))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('investments/my-investments'));
    }

    public function test_mis_inversiones_hides_admin_adjustment_container_investment(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $realInvestment */
        $realInvestment = $investor->investments()->create([
            'amount' => 500,
            'duration_months' => 6,
            'monthly_return_snapshot' => 5,
            'txid' => 'tx-visible-investment',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => now(),
            'confirmed_by' => $admin->id,
            'capital_balance' => 500,
        ]);

        InvestmentLedger::applyAdminBalanceCredit($investor, 100, 'Crédito oculto', $admin->id);

        $this->actingAs($investor)
            ->get(route('investments.mine'))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('investments/my-investments')
                ->has('investments', 1)
                ->where('investments.0.id', $realInvestment->id)
                ->where('investments.0.txid', 'tx-visible-investment')
                ->where('invested_any', true));

        $container = Investment::query()
            ->where('txid', InvestmentLedger::ADMIN_CREDIT_TXID_PREFIX.$investor->id)
            ->firstOrFail();

        $this->actingAs($investor)
            ->get(route('investments.transactions', $container))
            ->assertNotFound();
    }

    public function test_user_can_register_pending_investment_with_txid(): void
    {
        InvestmentSetting::current();

        $user = User::factory()->create();

        Mail::fake();

        $response = $this->actingAs($user)->post(route('investments.store'), [
            'funding_source' => 'blockchain',
            'amount' => 500,
            'duration_months' => 4,
            'txid' => 'trc20hash123unique',
        ]);

        $response->assertRedirect(route('investments.index'));

        $this->assertDatabaseMissing('investments', [
            'user_id' => $user->id,
        ]);

        Mail::assertSent(ConfirmInvestmentMail::class);

        $confirmUrl = null;
        Mail::assertSent(ConfirmInvestmentMail::class, function (ConfirmInvestmentMail $mail) use (&$confirmUrl): bool {
            $confirmUrl = $mail->confirmUrl;

            return true;
        });

        $this->assertIsString($confirmUrl);
        $this->assertSame(1, preg_match('#/confirmaciones/correo/inversion/([a-fA-F0-9]+)#', (string) $confirmUrl, $matches));
        $token = $matches[1];

        $this->actingAs($user)
            ->get(route('confirm.email.investment', ['token' => $token]))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('confirmations/investment')
                ->where('pending_approval', true)
                ->where('authenticated', true));

        $this->assertDatabaseHas('investments', [
            'user_id' => $user->id,
            'amount' => '500.00',
            'duration_months' => 4,
            'txid' => 'trc20hash123unique',
            'status' => InvestmentStatus::Pending->value,
        ]);
    }

    public function test_investor_can_create_active_investment_from_withdrawable_balance(): void
    {
        InvestmentSetting::query()->update([
            'min_amount' => 1,
        ]);

        $this->travelTo('2026-06-02 09:00:00');

        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $source */
        $source = $investor->investments()->create([
            'amount' => 2000,
            'duration_months' => 12,
            'monthly_return_snapshot' => 36,
            'txid' => 'tx-src-balance',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-05-01'),
            'maturity_at' => Carbon::parse('2027-05-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 2000,
        ]);
        InvestmentTransaction::query()->create([
            'investment_id' => $source->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 2000,
            'balance_after' => 2000,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        InvestmentLedger::applyDailyAccrualForDate(Carbon::parse('2026-06-02'));

        $source->refresh();
        $withdrawable = InvestmentLedger::totalWithdrawableForUser($investor->id);

        $this->assertGreaterThanOrEqual(1.0, $withdrawable);

        $reinvestAmount = round(min($withdrawable, 50), 2);
        $this->assertGreaterThanOrEqual(1.0, $reinvestAmount);

        Mail::fake();

        $this->actingAs($investor)
            ->post(route('investments.store'), [
                'funding_source' => 'balance',
                'amount' => $reinvestAmount,
                'duration_months' => 4,
                'txid' => '',
            ])
            ->assertRedirect(route('investments.index'));

        $this->assertSame(1, Investment::query()->where('user_id', $investor->id)->count());

        $confirmUrl = null;
        Mail::assertSent(ConfirmInvestmentMail::class, function (ConfirmInvestmentMail $mail) use (&$confirmUrl): bool {
            $confirmUrl = $mail->confirmUrl;

            return true;
        });
        $this->assertSame(1, preg_match('#/confirmaciones/correo/inversion/([a-fA-F0-9]+)#', (string) $confirmUrl, $matches));

        $this->actingAs($investor)
            ->get(route('confirm.email.investment', ['token' => $matches[1]]))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('confirmations/investment')
                ->where('pending_approval', false)
                ->where('authenticated', true));

        $this->assertSame(3, Investment::query()->where('user_id', $investor->id)->count());

        $this->assertDatabaseHas('investments', [
            'user_id' => $investor->id,
            'txid' => InvestmentLedger::ADMIN_CREDIT_TXID_PREFIX.$investor->id,
        ]);

        $latest = Investment::query()
            ->where('user_id', $investor->id)
            ->orderByDesc('id')
            ->first();

        $this->assertNotNull($latest);
        $this->assertSame(InvestmentStatus::Active, $latest->status);
        $this->assertStringStartsWith('INTERNAL-', $latest->txid);

        $this->travelBack();
    }

    public function test_investment_from_balance_fails_when_insufficient_withdrawable(): void
    {
        InvestmentSetting::query()->update([
            'min_amount' => 1,
        ]);

        $user = User::factory()->create();

        $this->actingAs($user)
            ->from(route('investments.index'))
            ->post(route('investments.store'), [
                'funding_source' => 'balance',
                'amount' => 100,
                'duration_months' => 4,
            ])
            ->assertSessionHasErrors('amount');

        $this->assertDatabaseMissing('investments', ['user_id' => $user->id]);
    }

    public function test_investment_from_balance_accepts_amount_below_product_minimum(): void
    {
        InvestmentSetting::current()->update([
            'min_amount' => 500,
            'monthly_return' => 36,
        ]);

        $this->travelTo('2026-06-02 09:00:00');

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        $investor->investments()->create([
            'amount' => 2000,
            'duration_months' => 12,
            'monthly_return_snapshot' => 36,
            'txid' => 'tx-src-small-reinv',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-05-01'),
            'maturity_at' => Carbon::parse('2027-05-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 2000,
        ]);
        $source = $investor->investments()->orderByDesc('id')->first();
        $this->assertNotNull($source);
        InvestmentTransaction::query()->create([
            'investment_id' => $source->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 2000,
            'balance_after' => 2000,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        InvestmentLedger::applyDailyAccrualForDate(Carbon::parse('2026-06-02'));

        $withdrawable = InvestmentLedger::totalWithdrawableForUser($investor->id);
        $this->assertGreaterThanOrEqual(20.0, $withdrawable);

        $small = min(42.5, $withdrawable);

        Mail::fake();

        $this->actingAs($investor)
            ->post(route('investments.store'), [
                'funding_source' => 'balance',
                'amount' => $small,
                'duration_months' => 4,
                'txid' => '',
            ])
            ->assertRedirect(route('investments.index'));

        Mail::assertSent(ConfirmInvestmentMail::class);

        $this->travelBack();
    }

    public function test_blockchain_investment_requires_product_minimum(): void
    {
        InvestmentSetting::query()->update([
            'min_amount' => 500,
        ]);
        InvestmentSetting::current();

        $user = User::factory()->create();

        Mail::fake();

        $this->actingAs($user)
            ->post(route('investments.store'), [
                'funding_source' => 'blockchain',
                'amount' => 100,
                'duration_months' => 4,
                'txid' => 'tx-below-product-min',
            ])
            ->assertSessionHasErrors('amount');

        Mail::assertNotSent(ConfirmInvestmentMail::class);
    }

    public function test_admin_can_confirm_pending_investment(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 1000,
            'duration_months' => 12,
            'monthly_return_snapshot' => 3.5,
            'txid' => 'tx-confirm-test',
            'status' => InvestmentStatus::Pending,
        ]);

        $response = $this->actingAs($admin)->patch(route('admin.investments.confirm', $investment));

        $response->assertRedirect(route('admin.investments.index'));

        $investment->refresh();

        $this->assertSame(InvestmentStatus::Active, $investment->status);
        $this->assertNotNull($investment->maturity_at);
        $this->assertNotNull($investment->confirmed_at);
        $this->assertSame($admin->id, $investment->confirmed_by);
        $this->assertSame('1000.00', (string) $investment->capital_balance);

        $this->assertDatabaseHas('investment_transactions', [
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit->value,
            'balance_after' => '1000.00',
        ]);
    }

    public function test_daily_accrual_applies_plan_rate_over_calendar_days(): void
    {
        $this->travelTo('2026-05-15 08:00:00');

        $settings = InvestmentSetting::current();
        $settings->update(['monthly_return' => 3]);
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 200,
            'duration_months' => 4,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-daily-a',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-05-01 10:00:00'),
            'maturity_at' => Carbon::parse('2027-05-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 200,
        ]);
        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 200,
            'balance_after' => 200,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        $dayGain = round(200 * (3 / 100) / 31, 2);

        $credited = InvestmentLedger::applyDailyAccrualForDate(Carbon::parse('2026-05-15'));

        $this->assertSame(1, $credited);

        $investment->refresh();
        $this->assertSame((string) round(200 + $dayGain, 2), (string) $investment->capital_balance);

        $this->assertDatabaseHas('investment_transactions', [
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::DailyGain->value,
            'amount' => number_format($dayGain, 2, '.', ''),
        ]);

        $creditedTwice = InvestmentLedger::applyDailyAccrualForDate(Carbon::parse('2026-05-15'));
        $this->assertSame(0, $creditedTwice);

        $this->travelBack();
    }

    public function test_daily_accrual_never_exceeds_monthly_percent_cap_when_catching_up(): void
    {
        InvestmentSetting::current()->update(['monthly_return' => 4]);
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 200,
            'duration_months' => 4,
            'monthly_return_snapshot' => 4,
            'txid' => 'tx-monthly-cap',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-05-01'),
            'maturity_at' => Carbon::parse('2027-05-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 200,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 200,
            'balance_after' => 200,
        ]);

        for ($day = 1; $day <= 31; $day++) {
            InvestmentLedger::applyDailyAccrualForDate(
                Carbon::parse(sprintf('2026-05-%02d', $day)),
            );
        }

        $mayTotal = (float) InvestmentTransaction::query()
            ->where('investment_id', $investment->id)
            ->where('type', InvestmentTransactionType::DailyGain)
            ->whereBetween('accrual_on', ['2026-05-01', '2026-05-31'])
            ->sum('amount');

        $monthlyCap = round(200 * 0.04, 2);

        $this->assertLessThanOrEqual($monthlyCap, $mayTotal);
        $this->assertGreaterThan(7.5, $mayTotal);
    }

    public function test_investor_can_view_transaction_ledger_for_own_investment(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 100,
            'duration_months' => 3,
            'monthly_return_snapshot' => 5,
            'txid' => 'tx-ledger',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => now(),
            'confirmed_by' => $admin->id,
            'capital_balance' => 100,
        ]);
        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 100,
            'balance_after' => 100,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        $this->actingAs($investor)
            ->get(route('investments.transactions', $investment))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('investments/investment-transactions')
                ->has('transactions.data', 1)
                ->where('transactions.total', 1));
    }

    public function test_admin_can_register_withdrawal(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 300,
            'duration_months' => 6,
            'monthly_return_snapshot' => 5,
            'txid' => 'tx-wd',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => now(),
            'confirmed_by' => $admin->id,
            'capital_balance' => 300,
        ]);
        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 300,
            'balance_after' => 300,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        $this->actingAs($admin)
            ->post(route('admin.users.withdrawal.store', $investor), [
                'amount' => 100,
            ])
            ->assertRedirect(route('admin.users.edit', $investor));

        $container = Investment::query()
            ->where('txid', InvestmentLedger::ADMIN_CREDIT_TXID_PREFIX.$investor->id)
            ->first();

        $this->assertNotNull($container);
        $this->assertSame('200.00', (string) $container->capital_balance);

        $this->assertDatabaseHas('investment_transactions', [
            'investment_id' => $container->id,
            'type' => InvestmentTransactionType::Withdrawal->value,
            'amount' => '100.00',
            'balance_after' => '200.00',
        ]);
    }

    public function test_guest_cannot_update_investment_settings(): void
    {
        $response = $this->put(route('admin.investment-settings.update'), [
            'min_amount' => 100,
            'max_amount' => 10000,
            'monthly_return' => 2.5,
            'payment_method' => 'TRC20',
            'payment_address' => 'TWALLET123',
            'durations' => [3, 6, 9],
        ]);

        $response->assertRedirect(route('login'));

        $settings = InvestmentSetting::current();

        $this->assertNotSame('100.00', (string) $settings->min_amount);
    }

    public function test_non_admin_user_cannot_manage_investment_settings(): void
    {
        $user = User::factory()->create([
            'is_admin' => false,
        ]);

        $this->actingAs($user)
            ->get(route('admin.investment-settings.edit'))
            ->assertForbidden();

        $this->actingAs($user)
            ->put(route('admin.investment-settings.update'), [
                'min_amount' => 100,
                'max_amount' => 10000,
                'monthly_return' => 2.5,
                'payment_method' => 'TRC20',
                'payment_address' => 'TWALLET123',
                'require_kyc_to_invest' => false,
                'require_kyc_to_withdraw' => true,
                'referral_direct_percent' => 7,
                'referral_indirect_percent' => 5,
                'referral_direct_earning_limit' => null,
                'referral_indirect_earning_limit' => null,
                'withdrawal_fee_percent' => 0,
                'withdrawal_fee_fixed' => 0,
                'min_withdrawal_amount' => 10,
                'durations' => [4, 8, 12],
            ])
            ->assertForbidden();

        $this->actingAs($user)
            ->get(route('admin.investments.index'))
            ->assertForbidden();
    }

    public function test_admin_can_view_accrual_info_page(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();

        $this->actingAs($admin)
            ->get(route('admin.platform-accrual.index'))
            ->assertOk()
            ->assertInertia(fn ($page) => $page->component('admin/platform-monthly-accrual'));
    }

    public function test_admin_investment_settings_can_be_updated_when_authenticated(): void
    {
        $admin = User::factory()->admin()->create();
        InvestmentSetting::current();
        DB::table('investment_settings')->update(['accrual_frequency' => AccrualFrequency::Daily->value]);

        $response = $this->actingAs($admin)->put(route('admin.investment-settings.update'), [
            'min_amount' => 100,
            'max_amount' => 10000,
            'monthly_return' => 2.5,
            'payment_method' => 'TRC20',
            'payment_address' => 'TWALLET123',
            'require_kyc_to_invest' => false,
            'require_kyc_to_withdraw' => true,
            'referral_direct_percent' => 7,
            'referral_indirect_percent' => 5,
            'referral_direct_earning_limit' => 500,
            'referral_indirect_earning_limit' => null,
            'withdrawal_fee_percent' => 1.5,
            'withdrawal_fee_fixed' => 2,
            'min_withdrawal_amount' => 25,
            'accrual_day_of_month' => 15,
            'maintenance_mode_enabled' => false,
            'durations' => [3, 6, 9],
        ]);

        $response->assertRedirect(route('admin.investment-settings.edit'));

        $settings = InvestmentSetting::current();

        $this->assertSame('100.00', (string) $settings->min_amount);
        $this->assertSame('10000.00', (string) $settings->max_amount);
        $this->assertSame('2.50', (string) $settings->monthly_return);
        $this->assertSame('TRC20', $settings->payment_method);
        $this->assertSame('TWALLET123', $settings->payment_address);
        $this->assertFalse($settings->require_kyc_to_invest);
        $this->assertTrue($settings->require_kyc_to_withdraw);
        $this->assertSame('7.0000', (string) $settings->referral_direct_percent);
        $this->assertSame('5.0000', (string) $settings->referral_indirect_percent);
        $this->assertSame('500.00', (string) $settings->referral_direct_earning_limit);
        $this->assertNull($settings->referral_indirect_earning_limit);
        $this->assertSame('1.5000', (string) $settings->withdrawal_fee_percent);
        $this->assertSame('2.00', (string) $settings->withdrawal_fee_fixed);
        $this->assertSame('25.00', (string) $settings->min_withdrawal_amount);
        $this->assertSame(AccrualFrequency::Monthly, $settings->accrual_frequency);
        $this->assertSame(15, (int) $settings->accrual_day_of_month);
        $this->assertSame(
            [3, 6, 9],
            InvestmentDuration::query()
                ->where('is_active', true)
                ->orderBy('sort_order')
                ->pluck('months')
                ->toArray(),
        );
    }

    public function test_monthly_period_accrual_prorates_first_partial_segment(): void
    {
        $settings = InvestmentSetting::current();
        $settings->update([
            'monthly_return' => 3,
            'accrual_frequency' => AccrualFrequency::Monthly->value,
            'accrual_day_of_month' => 30,
        ]);

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 600,
            'duration_months' => 12,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-monthly-part',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-05-20 12:00:00'),
            'maturity_at' => Carbon::parse('2027-06-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 600,
        ]);
        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 600,
            'balance_after' => 600,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        $expectedGain = 0.0;
        for ($day = 20; $day <= 31; $day++) {
            $cursor = Carbon::parse(sprintf('2026-05-%02d', $day))->startOfDay();
            $expectedGain += 600 * (3 / 100) / max(1, $cursor->daysInMonth);
        }
        $expectedGain = round($expectedGain, 2);

        $this->assertSame(1, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-05-30')));
        $this->assertSame(0, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-05-30')));

        $investment->refresh();
        $this->assertSame(round(600 + $expectedGain, 2), round((float) $investment->capital_balance, 2));

        /** @var InvestmentTransaction|null $period */
        $period = InvestmentTransaction::query()
            ->where('investment_id', $investment->id)
            ->where('type', InvestmentTransactionType::PeriodAccrual)
            ->first();

        $this->assertNotNull($period);
        $this->assertSame('2026-05-20', $period->gain_period_start?->toDateString());
        $this->assertSame('2026-05-31', $period->gain_period_end?->toDateString());
        $this->assertSame('2026-05-30', $period->accrual_on?->toDateString());
    }

    public function test_monthly_period_accrual_full_calendar_month_gets_nominal_percent(): void
    {
        InvestmentSetting::current()->update([
            'monthly_return' => 3.5,
            'accrual_frequency' => AccrualFrequency::Monthly->value,
            'accrual_day_of_month' => 29,
        ]);

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 200,
            'duration_months' => 6,
            'monthly_return_snapshot' => 3.5,
            'txid' => 'tx-monthly-full',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-06-01'),
            'maturity_at' => Carbon::parse('2026-12-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 200,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 200,
            'balance_after' => 200,
        ]);

        $this->assertSame(1, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-06-29')));

        /** @var InvestmentTransaction $period */
        $period = InvestmentTransaction::query()
            ->where('investment_id', $investment->id)
            ->where('type', InvestmentTransactionType::PeriodAccrual)
            ->firstOrFail();

        $this->assertSame('2026-06-01', $period->gain_period_start?->toDateString());
        $this->assertSame('2026-06-30', $period->gain_period_end?->toDateString());
        $this->assertSame('2026-06-29', $period->accrual_on?->toDateString());
        $this->assertSame('7.00', number_format((float) $period->amount, 2, '.', ''));
    }

    public function test_monthly_period_accrual_credits_run_month_only_not_catch_up(): void
    {
        InvestmentSetting::current()->update([
            'monthly_return' => 5,
            'accrual_frequency' => AccrualFrequency::Monthly->value,
            'accrual_day_of_month' => 30,
        ]);

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 200,
            'duration_months' => 4,
            'monthly_return_snapshot' => 5,
            'txid' => 'tx-no-catch-up',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-06-10'),
            'maturity_at' => Carbon::parse('2026-10-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 200,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 200,
            'balance_after' => 200,
        ]);

        $this->assertSame(1, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-08-30')));

        /** @var InvestmentTransaction $period */
        $period = InvestmentTransaction::query()
            ->where('investment_id', $investment->id)
            ->where('type', InvestmentTransactionType::PeriodAccrual)
            ->firstOrFail();

        $this->assertSame('2026-08-01', $period->gain_period_start?->toDateString());
        $this->assertSame('2026-08-31', $period->gain_period_end?->toDateString());
        $this->assertSame('2026-08-30', $period->accrual_on?->toDateString());

        $schedule = UserInvestmentPortfolio::contractMonthScheduleForInvestment($investment->fresh());
        $june = collect($schedule)->first(fn (array $row): bool => ($row['is_skipped'] ?? false) && str_contains(
            UserInvestmentPortfolio::formatContractMonthLabel($row['start'], $row['end']),
            'Junio',
        ));
        $july = collect($schedule)->first(fn (array $row): bool => ($row['is_skipped'] ?? false) && str_contains(
            UserInvestmentPortfolio::formatContractMonthLabel($row['start'], $row['end']),
            'Julio',
        ));

        $this->assertNotNull($june);
        $this->assertNotNull($july);
    }

    public function test_referral_bonus_not_created_when_referrer_program_disabled(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $referrer = User::factory()->create(['referral_program_enabled' => false]);
        $investor = User::factory()->create(['referred_by_user_id' => $referrer->id]);

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 400,
            'duration_months' => 12,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-ref-off',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-05-10 09:00:00'),
            'maturity_at' => Carbon::parse('2027-06-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 400,
        ]);
        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 400,
            'balance_after' => 400,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        InvestmentLedger::creditReferralBonusesOnInvestmentApproval($investment);

        $this->assertDatabaseMissing('investment_transactions', [
            'beneficiary_user_id' => $referrer->id,
            'type' => InvestmentTransactionType::ReferralDirectBonus->value,
        ]);

        $this->assertSame(
            0,
            InvestmentTransaction::query()
                ->where('beneficiary_user_id', $referrer->id)
                ->whereIn('type', [
                    InvestmentTransactionType::ReferralDirectBonus->value,
                    InvestmentTransactionType::ReferralIndirectBonus->value,
                ])
                ->count(),
        );
    }

    public function test_admin_can_void_pending_investment(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 500,
            'duration_months' => 6,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-void-pending',
            'status' => InvestmentStatus::Pending,
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.investments.void', $investment))
            ->assertRedirect(route('admin.investments.index'));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Cancelled, $investment->status);
        $this->assertDatabaseMissing('investment_transactions', [
            'investment_id' => $investment->id,
        ]);
    }

    public function test_admin_can_void_active_investment_and_liquidate_capital(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 1000,
            'duration_months' => 12,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-void-active',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => now(),
            'maturity_at' => now()->addYear(),
            'confirmed_by' => $admin->id,
            'capital_balance' => 1050,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 1000,
            'balance_after' => 1000,
            'platform_month_accrual_id' => null,
            'investment_month_index' => null,
            'applied_percent' => null,
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.investments.void', $investment))
            ->assertRedirect(route('admin.investments.index'));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Cancelled, $investment->status);
        $this->assertSame('0.00', (string) $investment->capital_balance);

        $this->assertDatabaseHas('investment_transactions', [
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Withdrawal->value,
            'amount' => '1050.00',
        ]);
    }

    public function test_investment_is_marked_completed_and_fully_withdrawable_after_final_accrual(): void
    {
        InvestmentSetting::current()->update([
            'monthly_return' => 3,
            'accrual_frequency' => AccrualFrequency::Monthly->value,
            'accrual_day_of_month' => 29,
        ]);

        $this->travelTo('2026-07-15 12:00:00');

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 100,
            'duration_months' => 2,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-final-accrual',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-06-01'),
            'maturity_at' => Carbon::parse('2026-08-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 100,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 100,
            'balance_after' => 100,
        ]);

        $this->assertSame(1, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-06-29')));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Active, $investment->status);
        $this->assertLessThan(100.0, InvestmentLedger::withdrawableFromInvestment($investment));

        $this->assertSame(1, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-07-29')));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Completed, $investment->status);
        $this->assertSame(2, $investment->months_credited);
        $this->assertSame(
            round((float) $investment->capital_balance, 2),
            InvestmentLedger::withdrawableFromInvestment($investment),
        );
    }

    public function test_investment_is_marked_completed_when_contract_ends(): void
    {
        $this->travelTo('2027-06-15 12:00:00');

        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 50,
            'duration_months' => 12,
            'monthly_return_snapshot' => 2.5,
            'txid' => 'tx-completed-test',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-05-01'),
            'maturity_at' => Carbon::parse('2027-05-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 50.54,
            'months_credited' => 0,
        ]);

        $finalized = InvestmentLedger::finalizeEndedActiveInvestments();

        $this->assertSame(1, $finalized);

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Completed, $investment->status);
        $this->assertSame(12, $investment->months_credited);
    }

    public function test_admin_can_reactivate_completed_investment(): void
    {
        InvestmentSetting::current();
        $this->travelTo('2026-08-15 12:00:00');

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 200,
            'duration_months' => 6,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-reactivate',
            'status' => InvestmentStatus::Completed,
            'confirmed_at' => Carbon::parse('2026-01-01'),
            'maturity_at' => Carbon::parse('2026-07-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 236,
            'months_credited' => 6,
            'accrual_through_date' => '2026-06-30',
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 200,
            'balance_after' => 200,
        ]);

        $this->assertSame(236.0, InvestmentLedger::totalWithdrawableForUser($investor->id));

        $this->actingAs($admin)
            ->patch(route('admin.investments.reactivate', $investment), ['tab' => 'completed'])
            ->assertRedirect(route('admin.investments.index', ['tab' => 'completed']));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Active, $investment->status);
        $this->assertSame(6, $investment->duration_months);
        $this->assertSame('2026-08-16', $investment->maturity_at?->toDateString());
        $this->assertSame(6, $investment->months_credited);
        $this->assertSame(36.0, InvestmentLedger::totalWithdrawableForUser($investor->id));
    }

    public function test_admin_can_reactivate_completed_investment_with_contract_extension(): void
    {
        InvestmentSetting::current();
        $this->travelTo('2026-08-15 12:00:00');

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 200,
            'duration_months' => 6,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-reactivate-extension',
            'status' => InvestmentStatus::Completed,
            'confirmed_at' => Carbon::parse('2026-01-01'),
            'maturity_at' => Carbon::parse('2026-07-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 236,
            'months_credited' => 6,
            'accrual_through_date' => '2026-06-30',
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 200,
            'balance_after' => 200,
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.investments.reactivate', $investment), [
                'tab' => 'completed',
                'extension_months' => 3,
            ])
            ->assertRedirect(route('admin.investments.index', ['tab' => 'completed']));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Active, $investment->status);
        $this->assertSame(9, $investment->duration_months);
        $this->assertSame('2026-11-15', $investment->maturity_at?->toDateString());
        $this->assertSame(6, $investment->months_credited);
        $this->assertSame(36.0, InvestmentLedger::totalWithdrawableForUser($investor->id));
    }

    public function test_admin_can_manually_finalize_fully_accredited_investment_when_adjustments_enabled(): void
    {
        config(['app.allow_adjustments' => true]);

        InvestmentSetting::current()->update([
            'monthly_return' => 3,
            'accrual_frequency' => AccrualFrequency::Monthly->value,
            'accrual_day_of_month' => 29,
        ]);

        $this->travelTo('2026-07-15 12:00:00');

        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 100,
            'duration_months' => 2,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-manual-finalize',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-06-01'),
            'maturity_at' => Carbon::parse('2026-09-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 100,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 100,
            'balance_after' => 100,
        ]);

        $this->assertSame(1, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-06-29')));
        $this->assertSame(1, InvestmentLedger::runAccrualForDate(Carbon::parse('2026-07-29')));

        $investment->refresh();
        $this->assertSame(2, InvestmentLedger::accreditedContractMonths($investment));
        $investment->update(['status' => InvestmentStatus::Active]);

        $this->actingAs($admin)
            ->patch(route('admin.investments.finalize', $investment), ['tab' => 'active'])
            ->assertRedirect(route('admin.investments.index', ['tab' => 'active']));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Completed, $investment->status);
        $this->assertSame(2, $investment->months_credited);
    }

    public function test_manual_finalize_is_hidden_when_adjustments_disabled(): void
    {
        config(['app.allow_adjustments' => false]);

        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 100,
            'duration_months' => 2,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-finalize-hidden',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-06-01'),
            'maturity_at' => Carbon::parse('2026-08-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 106,
            'months_credited' => 2,
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.investments.finalize', $investment))
            ->assertNotFound();
    }

    public function test_admin_cannot_reactivate_non_completed_investment(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $investor = User::factory()->create();

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 100,
            'duration_months' => 4,
            'monthly_return_snapshot' => 3,
            'txid' => 'tx-still-active',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => now(),
            'maturity_at' => now()->addMonths(4),
            'confirmed_by' => $admin->id,
            'capital_balance' => 100,
        ]);

        $this->actingAs($admin)
            ->patch(route('admin.investments.reactivate', $investment))
            ->assertRedirect(route('admin.investments.index'));

        $investment->refresh();
        $this->assertSame(InvestmentStatus::Active, $investment->status);
    }

    public function test_admin_investments_index_filters_by_search_query(): void
    {
        InvestmentSetting::current();
        $admin = User::factory()->admin()->create();
        $alice = User::factory()->create(['email' => 'alice@filter.test']);
        $bob = User::factory()->create(['email' => 'bob@filter.test']);

        $alice->investments()->create([
            'amount' => 100,
            'duration_months' => 3,
            'monthly_return_snapshot' => 3,
            'txid' => 'unique-alice-txid',
            'status' => InvestmentStatus::Pending,
        ]);

        $bob->investments()->create([
            'amount' => 200,
            'duration_months' => 3,
            'monthly_return_snapshot' => 3,
            'txid' => 'unique-bob-txid',
            'status' => InvestmentStatus::Pending,
        ]);

        $this->actingAs($admin)
            ->get(route('admin.investments.index', ['q' => 'alice@filter', 'tab' => 'pending']))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('admin/investments-overview')
                ->has('investments.data', 1)
                ->where('investments.data.0.txid', 'unique-alice-txid')
                ->where('investments.per_page', 15));
    }

    public function test_admin_can_view_investment_accrual_detail(): void
    {
        InvestmentSetting::current()->update(['monthly_return' => 10]);
        $admin = User::factory()->admin()->create();
        $user = User::factory()->create();

        /** @var Investment $investment */
        $investment = $user->investments()->create([
            'amount' => 500,
            'duration_months' => 4,
            'monthly_return_snapshot' => 10,
            'txid' => 'tx-accrual-detail',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-01-01'),
            'maturity_at' => Carbon::parse('2027-01-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 510,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::DailyGain,
            'amount' => 10,
            'balance_after' => 510,
            'accrual_on' => '2026-01-15',
            'applied_percent' => 0.032258,
        ]);

        $this->actingAs($admin)
            ->get(route('admin.investments.accruals', $investment))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->component('admin/investment-accruals')
                ->where('investment.txid', 'tx-accrual-detail')
                ->has('lines', 1)
                ->where('lines.0.amount_usd', '10.00')
                ->where('lines.0.referral_paid', false));
    }

    public function test_admin_accrual_detail_lists_referrals_for_specific_gain_line(): void
    {
        InvestmentSetting::current()->update([
            'monthly_return' => 10,
            'referral_direct_percent' => 7,
            'referral_indirect_earning_limit' => 0,
        ]);

        $admin = User::factory()->admin()->create();
        $referrer = User::factory()->create(['referral_program_enabled' => true]);
        $investor = User::factory()->create(['referred_by_user_id' => $referrer->id]);

        /** @var Investment $investment */
        $investment = $investor->investments()->create([
            'amount' => 1000,
            'duration_months' => 4,
            'monthly_return_snapshot' => 10,
            'txid' => 'tx-ref-detail',
            'status' => InvestmentStatus::Active,
            'confirmed_at' => Carbon::parse('2026-01-01'),
            'maturity_at' => Carbon::parse('2027-01-01'),
            'confirmed_by' => $admin->id,
            'capital_balance' => 1000,
        ]);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'type' => InvestmentTransactionType::Deposit,
            'amount' => 1000,
            'balance_after' => 1000,
        ]);

        InvestmentLedger::applyMonthlyPeriodAccrualForDate(Carbon::parse('2026-01-30'));

        $gain = InvestmentTransaction::query()
            ->where('investment_id', $investment->id)
            ->where('type', InvestmentTransactionType::PeriodAccrual)
            ->first();

        $this->assertNotNull($gain);

        InvestmentTransaction::query()->create([
            'investment_id' => $investment->id,
            'beneficiary_user_id' => $referrer->id,
            'type' => InvestmentTransactionType::ReferralDirectBonus,
            'amount' => 70,
            'balance_after' => 70,
            'applied_percent' => 7,
            'referral_level' => 1,
            'referral_source_transaction_id' => $gain->id,
        ]);

        $this->actingAs($admin)
            ->get(route('admin.investments.accruals', $investment))
            ->assertOk()
            ->assertInertia(fn ($page) => $page
                ->where('lines.0.referral_paid', true)
                ->where('lines.0.referrals.0.beneficiary_name', trim("{$referrer->first_name} {$referrer->last_name}"))
                ->where('lines.0.referrals.0.type_label', 'Directo (nivel 1)'));
    }
}
