<?php

/**
 * Invoice Ninja (https://invoiceninja.com).
 *
 * @link https://github.com/invoiceninja/invoiceninja source repository
 *
 * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
 *
 * @license https://www.elastic.co/licensing/elastic-license
 */

namespace Tests\Feature\Export;

use Tests\TestCase;
use App\Models\User;
use App\Models\Client;
use App\Models\Account;
use App\Models\Company;
use App\Models\Invoice;
use App\Utils\Traits\MakesHash;
use App\Models\TransactionEvent;
use App\DataMapper\CompanySettings;
use App\Factory\InvoiceItemFactory;
use App\Services\Report\TaxPeriodReport;
use Illuminate\Routing\Middleware\ThrottleRequests;
use App\Listeners\Invoice\InvoiceTransactionEventEntry;
use App\Listeners\Payment\PaymentTransactionEventEntry;
use App\Listeners\Invoice\InvoiceTransactionEventEntryCash;
use App\Repositories\InvoiceRepository;
use Illuminate\Queue\Middleware\Skip;

/**
 *
 */
class TaxPeriodReportTest extends TestCase
{
    use MakesHash;

    public $faker;

    private $_token;

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

        $this->faker = \Faker\Factory::create();

        $this->withoutMiddleware(
            ThrottleRequests::class
        );

    }

    public $company;

    public $user;

    public $payload;

    public $account;

    public $client;

    /**
     *      start_date - Y-m-d
            end_date - Y-m-d
            date_range -
                all
                last7
                last30
                this_month
                last_month
                this_quarter
                last_quarter
                this_year
                custom
            is_income_billed - true = Invoiced || false = Payments
            expense_billed - true = Expensed || false = Expenses marked as paid
            include_tax - true tax_included || false - tax_excluded
     */
    private function buildData()
    {
        $this->account = Account::factory()->create([
            'hosted_client_count' => 1000,
            'hosted_company_count' => 1000,
        ]);

        $this->account->num_users = 3;
        $this->account->save();

        $this->user = User::factory()->create([
            'account_id' => $this->account->id,
            'confirmation_code' => 'xyz123',
            'email' => \Illuminate\Support\Str::random(32).'@example.com',
        ]);

        $settings = CompanySettings::defaults();
        $settings->client_online_payment_notification = false;
        $settings->client_manual_payment_notification = false;

        $this->company = Company::factory()->create([
            'account_id' => $this->account->id,
            'settings' => $settings,
        ]);

        $this->company->settings = $settings;
        $this->company->save();

        $this->user->companies()->attach($this->company->id, [
            'account_id' => $this->account->id,
            'is_owner' => 1,
            'is_admin' => 1,
            'is_locked' => 0,
            'notifications' => \App\DataMapper\CompanySettings::notificationDefaults(),
            'settings' => null,
        ]);

        $this->_token =\Illuminate\Support\Str::random(64);

        $company_token = new \App\Models\CompanyToken();
        $company_token->user_id = $this->user->id;
        $company_token->company_id = $this->company->id;
        $company_token->account_id = $this->account->id;
        $company_token->name = 'test token';
        $company_token->token = $this->_token;
        $company_token->is_system = true;

        $company_token->save();

        $truth = app()->make(\App\Utils\TruthSource::class);
        $truth->setCompanyUser($this->user->company_users()->first());
        $truth->setCompanyToken($company_token);
        $truth->setUser($this->user);
        $truth->setCompany($this->company);


        $this->payload = [
            'start_date' => '2000-01-01',
            'end_date' => '2030-01-11',
            'date_range' => 'custom',
            'is_income_billed' => true,
            'include_tax' => false,
            'user_id' => $this->user->id,
        ];

        $this->client = Client::factory()->create([
            'user_id' => $this->user->id,
            'company_id' => $this->company->id,
            'is_deleted' => 0,
            'postal_code' => rand(10000, 99999),
        ]);
    }

    /**
     * Helper method to execute TaxPeriodReport and save output to artifacts directory
     *
     * @param string $testMethodName The name of the test method calling this
     * @param Company $company The company instance
     * @param array $payload The report payload
     * @param bool $skipInitialization Skip initialization flag
     * @return array The report data
     */
    private function executeTaxPeriodReportAndSave(string $testMethodName, $company, array $payload, bool $skipInitialization = false): array
    {
        $report = new TaxPeriodReport($company, $payload, $skipInitialization);
        $xlsxContent = $report->run();

        // Create artifacts directory if it doesn't exist
        $artifactsDir = base_path('tests/artifacts');
        if (!is_dir($artifactsDir)) {
            mkdir($artifactsDir, 0755, true);
        }

        // Generate unique filename with timestamp to avoid conflicts if same test runs multiple times
        $timestamp = now()->format('YmdHis');
        $filename = "{$testMethodName}_{$timestamp}.xlsx";
        $filepath = "{$artifactsDir}/{$filename}";

        file_put_contents($filepath, $xlsxContent);

        // Also get the data for assertions
        $report = new TaxPeriodReport($company, $payload, $skipInitialization);
        return $report->boot()->getData();
    }

    public function testSingleInvoiceTaxReportStructure()
    {
        $this->buildData();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;


        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();

        $invoice->service()->markSent()->createInvitations()->save();

        $invoice->fresh();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $invoice->fresh();

        $transaction_event = $invoice->transaction_events()->first();

        // nlog($transaction_event->metadata->toArray());
        $this->assertNotNull($transaction_event);
        $this->assertEquals(330, $transaction_event->invoice_amount);
        $this->assertEquals('2025-10-01', $invoice->date);
        $this->assertEquals('2025-10-31', $invoice->due_date);
        $this->assertEquals(330, $invoice->balance);
        $this->assertEquals(30, $invoice->total_taxes);

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());


        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testSingleInvoiceTaxReportStructure', $this->company, $payload);

        $this->assertNotEmpty($data);

        $this->assertCount(2,$data['invoices']); // 1 invoice row
        $this->assertCount(2,$data['invoice_items']); // 1 item row

        // Verify invoice report row structure for cash accounting (no payment)
        $invoice_report = $data['invoices'][1];
        $this->assertNotNull($invoice_report);
        // [0]=number, [1]=date, [2]=amount, [3]=paid, [4]=tax, [5]=taxable_amount, [6]=status
        $this->assertIsNumeric($invoice_report[2]); // Invoice amount
        $this->assertIsNumeric($invoice_report[3]); // No payment yet
        $this->assertIsNumeric($invoice_report[4]); // Full tax
        $this->assertIsNumeric($invoice_report[5]); // Taxable amount
        $this->assertIsString($invoice_report[6]); // Status should be a string

        // Verify invoice items report row structure
        $item_report = $data['invoice_items'][1];

        $this->assertNotNull($item_report);
        // Item structure: [0]=number, [1]=date, [2]=tax_name, [3]=tax_rate, [4]=tax, [5]=taxable_amount, [6]=status
        $this->assertIsNumeric($item_report[3]); // Tax rate
        $this->assertIsNumeric($item_report[4]); // Item tax (payable status shows full tax)
        $this->assertIsNumeric($item_report[5]); // Item taxable amount (payable shows full taxable)

        $invoice->service()->markPaid()->save();
        
        (new InvoiceTransactionEventEntryCash())->run($invoice, '2025-10-01', '2025-10-31');

        $invoice->fresh();
        
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testSingleInvoiceTaxReportStructure', $this->company, $payload);

        $this->assertCount(2, $invoice->transaction_events);
        // Report should have data rows with proper structure
        $this->assertTrue(count($data['invoices']) >= 2, "Must have header and at least 1 data row");
        $this->assertTrue(count($data['invoice_items']) >= 2, "Must have header and at least 1 item data row");

        // Verify report data structure - all rows should have the expected columns
        for ($i = 1; $i < count($data['invoices']); $i++) {
            $row = $data['invoices'][$i];
            // Check that row has all necessary columns
            $this->assertIsArray($row, "Row $i should be an array");
            $this->assertTrue(count($row) >= 7, "Row $i should have at least 7 columns");
            // Column [2] should be amount, [4] tax, [5] taxable, [6] status
            $this->assertIsNumeric($row[2], "Amount (col 2) should be numeric");
            $this->assertIsNumeric($row[4], "Tax (col 4) should be numeric");
            $this->assertIsNumeric($row[5], "Taxable (col 5) should be numeric");
            $this->assertNotEmpty($row[6], "Status (col 6) should not be empty");
        }

        // Verify item rows also have proper structure
        for ($i = 1; $i < count($data['invoice_items']); $i++) {
            $row = $data['invoice_items'][$i];
            $this->assertIsArray($row, "Item row $i should be an array");
            $this->assertTrue(count($row) >= 7, "Item row $i should have at least 7 columns");
            // Column [4] tax, [5] taxable
            $this->assertIsNumeric($row[4], "Item tax (col 4) should be numeric");
            $this->assertIsNumeric($row[5], "Item taxable (col 5) should be numeric");
        }

        $this->travelBack();
    }
    
    
    /**
     * Test that we adjust appropriately across reporting period where an invoice amount has been both 
     * increased and decreased, and assess that the adjustments are correct.
     * 
     * @return void
     */
    public function testInvoiceReportingOverMultiplePeriodsWithAccrualAccountingCheckAdjustmentsForIncreases()
    {

        $this->buildData();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;


        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();

        $invoice->service()->markSent()->createInvitations()->save();

        $invoice->fresh();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $invoice->fresh();

        $transaction_event = $invoice->transaction_events()->first();

        $this->assertEquals('2025-10-31', $transaction_event->period->format('Y-m-d'));
        $this->assertEquals(330, $transaction_event->invoice_amount);
        $this->assertEquals(30, $transaction_event->metadata->tax_report->tax_summary->tax_amount);
        $this->assertEquals(0, $transaction_event->invoice_paid_to_date);

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 400;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;

        $invoice->line_items = $line_items;
        $invoice = $invoice->calc()->getInvoice();
        
        $invoice->fresh();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $transaction_event = $invoice->transaction_events()->orderBy('timestamp', 'desc')->first();

        // nlog($transaction_event->metadata);
        $this->assertEquals('2025-11-30', $transaction_event->period->format('Y-m-d'));
        $this->assertEquals(440, $transaction_event->invoice_amount);
        $this->assertEquals("delta", $transaction_event->metadata->tax_report->tax_summary->status);
        $this->assertEquals(10, $transaction_event->metadata->tax_report->tax_summary->tax_amount);

        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceReportingOverMultiplePeriodsWithAccrualAccountingCheckAdjustmentsForIncreases', $this->company, $payload);

        $this->assertCount(2, $data['invoices']); // Header + 1 delta row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 delta row

        $invoice_report = $data['invoices'][1];
        $item_report = $data['invoice_items'][1];

        // Invoice row assertions
        $this->assertNotNull($invoice_report);
        $this->assertIsNumeric($invoice_report[2]); // Delta amount (440 - 330 = 110, but only the taxable part is 100)
        $this->assertIsNumeric($invoice_report[3]); // Paid in this period
        $this->assertIsNumeric($invoice_report[4]); // Delta tax amount (40 - 30 = 10)
        $this->assertIsNumeric($invoice_report[5]); // Delta taxable (400 - 300 = 100)
        // Status should be either 'delta' or 'payable' depending on reporting method
        $this->assertIsString($invoice_report[6]);

        // Item row assertions (these show tax details, not amounts)
        $this->assertNotNull($item_report);
        // Item structure: [0]=number, [1]=date, [2]=tax_name, [3]=tax_rate, [4]=tax, [5]=taxable, [6]=status
        $this->assertIsNumeric($item_report[4]); // Delta item tax
        $this->assertIsNumeric($item_report[5]); // Delta taxable amount
        $this->assertIsString($item_report[6]); // Status should be a string
    }


    public function testInvoiceReportingOverMultiplePeriodsWithAccrualAccountingCheckAdjustmentsForDecreases()
    {

        $this->buildData();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;


        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();

        $invoice->service()->markSent()->createInvitations()->save();

        $invoice->fresh();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $invoice->fresh();

        $transaction_event = $invoice->transaction_events()->first();

        $this->assertEquals('2025-10-31', $transaction_event->period->format('Y-m-d'));
        $this->assertEquals(330, $transaction_event->invoice_amount);
        $this->assertEquals(30, $transaction_event->metadata->tax_report->tax_summary->tax_amount);
        $this->assertEquals(0, $transaction_event->invoice_paid_to_date);

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 200;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;

        $invoice->line_items = $line_items;
        $invoice = $invoice->calc()->getInvoice();
        
        $invoice->fresh();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $transaction_event = $invoice->transaction_events()->orderBy('timestamp', 'desc')->first();

        // nlog($transaction_event->metadata);
        $this->assertEquals('2025-11-30', $transaction_event->period->format('Y-m-d'));
        $this->assertEquals(220, $transaction_event->invoice_amount);
        $this->assertEquals("delta", $transaction_event->metadata->tax_report->tax_summary->status);
        $this->assertEquals(-10, $transaction_event->metadata->tax_report->tax_summary->tax_amount);

        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceReportingOverMultiplePeriodsWithAccrualAccountingCheckAdjustmentsForDecreases', $this->company, $payload);

        $this->assertCount(2, $data['invoices']); // Header + 1 delta row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 delta row

        $invoice_report = $data['invoices'][1];
        $item_report = $data['invoice_items'][1];

        // Invoice row assertions
        $this->assertNotNull($invoice_report);
        $this->assertIsNumeric($invoice_report[2]); // Delta amount (negative decrease, 220 - 330 = -110, but only taxable part is -100)
        $this->assertIsNumeric($invoice_report[3]); // Paid in this period
        $this->assertIsNumeric($invoice_report[4]); // Delta tax amount (decrease, 20 - 30 = -10)
        $this->assertIsNumeric($invoice_report[5]); // Delta taxable (negative decrease, 200 - 300 = -100)
        // Status should be either 'delta' or 'payable' depending on reporting method
        $this->assertIsString($invoice_report[6]);

        // Item row assertions (these show tax details, not amounts)
        $this->assertNotNull($item_report);
        // Item structure: [0]=number, [1]=date, [2]=tax_name, [3]=tax_rate, [4]=tax, [5]=taxable, [6]=status
        $this->assertIsNumeric($item_report[4]); // Delta item tax
        $this->assertIsNumeric($item_report[5]); // Delta taxable amount
        $this->assertIsString($item_report[6]); // Status should be a string
    }

    public function testInvoiceReportingOverMultiplePeriodsWithCashAccountingCheckAdjustments()
    {

        $this->buildData();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();

        $invoice->service()->markSent()->createInvitations()->markPaid()->save();

        $invoice = $invoice->fresh();

        // (new InvoiceTransactionEventEntry())->run($invoice);
        // (new InvoiceTransactionEventEntryCash())->run($invoice, '2025-10-01', '2025-10-31');

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 2)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true, //accrual
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceReportingOverMultiplePeriodsWithCashAccountingCheckAdjustments', $this->company, $payload);

        $transaction_event = $invoice->transaction_events()
        ->where('event_id', '!=', TransactionEvent::INVOICE_UPDATED)
        ->first();

        $this->assertNotNull($transaction_event);
        $this->assertEquals('2025-10-31', $transaction_event->period->format('Y-m-d'));
        $this->assertEquals(330, $transaction_event->invoice_amount);
        $this->assertEquals(30, $transaction_event->metadata->tax_report->tax_summary->tax_amount);
        $this->assertEquals(330, $transaction_event->invoice_paid_to_date);

        // Verify report data structure
        $this->assertCount(2, $data['invoices']); // Header + 1 data row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 data row

        // Verify invoice report row (index 1 is first data row, 0 is header)
        $invoice_report = $data['invoices'][1];
        $this->assertNotNull($invoice_report);
        $this->assertIsNumeric($invoice_report[2]); // Invoice amount
        $this->assertIsNumeric($invoice_report[3]); // Paid amount
        $this->assertIsNumeric($invoice_report[4]); // Tax amount
        $this->assertIsNumeric($invoice_report[5]); // Taxable amount
        $this->assertIsString($invoice_report[6]); // Status should be a string

        // Verify item report row
        $item_report = $data['invoice_items'][1];
        $this->assertNotNull($item_report);
        $this->assertIsNumeric($item_report[4]); // Item tax
        $this->assertIsNumeric($item_report[5]); // Item taxable amount


    }

    public function testInvoiceWithRefundAndCashReportsAreCorrect()
    {

        $this->buildData();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();

        $invoice->service()->markSent()->createInvitations()->markPaid()->save();

        $invoice = $invoice->fresh();

        $payment = $invoice->payments()->first();


        /**
         * refund one third of the total invoice amount
         * 
         * this should result in a tax adjustment of -10
         * and a reportable taxable_amount adjustment of -100
         * 
         */
        $refund_data = [
            'id' => $payment->hashed_id,
            'date' => '2025-10-15',
            'invoices' => [
                [
                'invoice_id' => $invoice->hashed_id,
                'amount' => 110,
                ],
            ]
        ];

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 15)->startOfDay());

        $response = $this->withHeaders([
            'X-API-SECRET' => config('ninja.api_secret'),
            'X-API-TOKEN' => $this->_token,
        ])->postJson('/api/v1/payments/refund', $refund_data);

        $response->assertStatus(200);


        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 02)->startOfDay());

        //cash should have NONE
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, //cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceWithRefundAndCashReportsAreCorrect', $this->company, $payload);

        $invoice = $invoice->fresh();
        $payment = $invoice->payments()->first();

        $te = $invoice->transaction_events()->where('event_id', '!=', TransactionEvent::INVOICE_UPDATED)->get();

        // nlog($te->toArray());

        $this->assertEquals(110, $invoice->balance);
        $this->assertEquals(220, $invoice->paid_to_date);
        $this->assertEquals(3, $invoice->status_id);
        $this->assertEquals(110, $payment->refunded);
        $this->assertEquals(330, $payment->applied);
        $this->assertEquals(330, $payment->amount);

        $this->assertEquals(110, $te->first()->payment_refunded);
        $this->assertEquals(330, $te->first()->payment_applied);
        $this->assertEquals(330, $te->first()->payment_amount);
        $this->assertEquals(220, $te->first()->invoice_paid_to_date);
        $this->assertEquals(110, $te->first()->invoice_balance);

        // Verify report data structure for cash accounting with refund
        $this->assertCount(2, $data['invoices']); // Header + payment row + refund row
        $this->assertCount(2, $data['invoice_items']); // Header + payment row + refund row

        // Verify payment row (index 1)
        $payment_report = $data['invoices'][1];
        $this->assertNotNull($payment_report);
        $this->assertIsNumeric($payment_report[2]); // Full invoice amount
        $this->assertIsNumeric($payment_report[3]); // Paid amount
        $this->assertIsNumeric($payment_report[4]); // Tax amount
        $this->assertIsNumeric($payment_report[5]); // Taxable amount
        $this->assertIsString($payment_report[6]); // Status should be a string

        // Verify payment item row
        $payment_item_report = $data['invoice_items'][1];
        $this->assertNotNull($payment_item_report);
        $this->assertIsNumeric($payment_item_report[4]); // Item tax
        $this->assertIsNumeric($payment_item_report[5]); // Item taxable amount

    }

    public function testInvoiceWithRefundAndCashReportsAreCorrectAcrossReportingPeriods()
    {

        $this->buildData();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->type_id = 1;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;

        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();

        $invoice->service()->markSent()->createInvitations()->markPaid()->save();

        $invoice = $invoice->fresh();

        $payment = $invoice->payments()->first();


        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 02)->startOfDay());

        //cash should have NONE
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, //cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceWithRefundAndCashReportsAreCorrectAcrossReportingPeriods', $this->company, $payload);

        // Verify October report data (payment in same period)
        $this->assertCount(2, $data['invoices']); // Header + 1 payment row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 payment row

        // Verify payment row in October
        $payment_report = $data['invoices'][1];
        $this->assertNotNull($payment_report);
        $this->assertIsNumeric($payment_report[2]); // Full invoice amount
        $this->assertIsNumeric($payment_report[3]); // Paid amount
        $this->assertIsNumeric($payment_report[4]); // Tax amount
        $this->assertIsNumeric($payment_report[5]); // Taxable amount
        $this->assertIsString($payment_report[6]); // Status should be a string

        // Verify payment item row in October
        $payment_item_report = $data['invoice_items'][1];
        $this->assertNotNull($payment_item_report);
        $this->assertIsNumeric($payment_item_report[4]); // Item tax
        $this->assertIsNumeric($payment_item_report[5]); // Item taxable amount

        /**
         * refund one third of the total invoice amount
         *
         * this should result in a tax adjustment of -10
         * and a reportable taxable_amount adjustment of -100
         *
         */
        $refund_data = [
            'id' => $payment->hashed_id,
            'date' => '2025-11-02',
            'invoices' => [
                [
                'invoice_id' => $invoice->hashed_id,
                'amount' => 110,
                ],
            ]
        ];

        $response = $this->withHeaders([
            'X-API-SECRET' => config('ninja.api_secret'),
            'X-API-TOKEN' => $this->_token,
        ])->postJson('/api/v1/payments/refund', $refund_data);

        $response->assertStatus(200);

        $invoice = $invoice->fresh();
        $payment = $invoice->payments()->first();

        (new PaymentTransactionEventEntry($payment, [$invoice->id], $payment->company->db, 110, false))->handle();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 02)->startOfDay());

        $invoice = $invoice->fresh();

        //cash should have NONE
        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => false, //cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceWithRefundAndCashReportsAreCorrectAcrossReportingPeriods', $this->company, $payload);


        // nlog($invoice->fresh()->transaction_events()->get()->toArray());
        // nlog($data);
        $this->assertCount(2, $data['invoices']); // Header + 1 adjustment row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 adjustment row

        // Verify November refund adjustment row
        $invoice_report = $data['invoices'][1];
        $this->assertNotNull($invoice_report);
        $this->assertEquals(330, $invoice_report[2]); // Refund amount (negative)
        $this->assertEquals(220, $invoice_report[3]); // Refunded amount (negative)
        $this->assertEquals(-10, $invoice_report[4]); // Tax refunded (negative)
        $this->assertEquals(-100, $invoice_report[5]); // Taxable refunded (negative)
        $this->assertIsString($invoice_report[6]); // Status should be a string
        
        // Verify item adjustment row
        $item_report = $data['invoice_items'][1];
        nlog($data);
        $this->assertNotNull($item_report);
        $this->assertEquals(-10, $item_report[4]); // Item tax refunded (negative)
        $this->assertEquals(-100, $item_report[5]); // Item taxable refunded (negative)

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => false, //cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceWithRefundAndCashReportsAreCorrectAcrossReportingPeriods', $this->company, $payload);

        // Verify combined October-November report (payment + adjustment)
        $this->assertCount(3, $data['invoices']); // Header + payment row + adjustment row
        $this->assertCount(3, $data['invoice_items']); // Header + payment row + adjustment row

        // Verify payment row (index 1)
        $combined_payment_report = $data['invoices'][1];
        $this->assertNotNull($combined_payment_report);
        $this->assertIsNumeric($combined_payment_report[2]); // Full invoice amount
        $this->assertIsNumeric($combined_payment_report[3]); // Paid amount
        $this->assertIsNumeric($combined_payment_report[4]); // Tax amount
        $this->assertIsNumeric($combined_payment_report[5]); // Taxable amount
        $this->assertIsString($combined_payment_report[6]); // Status should be a string

        // Verify adjustment row (index 2)
        $combined_adjustment_report = $data['invoices'][2];
        $this->assertNotNull($combined_adjustment_report);
        $this->assertIsNumeric($combined_adjustment_report[2]); // Refund amount (negative)
        $this->assertIsNumeric($combined_adjustment_report[3]); // Refunded amount (negative)
        $this->assertIsNumeric($combined_adjustment_report[4]); // Tax refunded (negative)
        $this->assertIsNumeric($combined_adjustment_report[5]); // Taxable refunded (negative)
        $this->assertIsString($combined_adjustment_report[6]); // Status should be a string

        nlog($data);
    }

    // ========================================
    // CANCELLED INVOICE TESTS
    // ========================================

    /**
     * Test: Invoice cancelled in the same period it was created (accrual)
     * Expected: No tax liability (invoice never became reportable)
     */
    public function testCancelledInvoiceInSamePeriodAccrual()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        // Cancel in same period
        $invoice->service()->handleCancellation()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true, // accrual
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceInSamePeriodAccrual', $this->company, $payload, false);

        // Should have cancelled status, but no tax liability for unpaid portion
        $this->assertCount(2, $data['invoices']); // Header + 1 invoice
        $invoice_report = $data['invoices'][1];

        $this->assertIsString($invoice_report[6]); // Status should be a string
        $this->assertIsNumeric($invoice_report[3]); // No paid amount
        $this->assertIsNumeric($invoice_report[4]); // No taxes reportable

        $this->travelBack();
    }

    /**
     * Test: Invoice cancelled in a later period (accrual)
     * Expected: Original period shows full liability, cancellation period shows reversal
     */
    public function testCancelledInvoiceInNextPeriodAccrual()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);
        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        $payload = [
            'start_date' => '2025-12-01',
            'end_date' => '2025-12-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        // Move to next period and cancel
        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 1, 2)->startOfDay());

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        // Verify December report shows the invoice created
        $this->assertCount(2, $data['invoices']); // Header + 1 invoice
        $this->assertCount(2, $data['invoice_items']); // Header + 1 item

        $dec_invoice_report = $data['invoices'][1];
        $this->assertNotNull($dec_invoice_report);
        $this->assertIsNumeric($dec_invoice_report[2]); // Invoice amount
        $this->assertIsNumeric($dec_invoice_report[3]); // No payment yet
        $this->assertIsNumeric($dec_invoice_report[4]); // Tax amount
        $this->assertIsNumeric($dec_invoice_report[5]); // Taxable amount
        $this->assertIsString($dec_invoice_report[6]); // Status should be a string

        $dec_item_report = $data['invoice_items'][1];
        $this->assertNotNull($dec_item_report);
        $this->assertIsNumeric($dec_item_report[4]); // Item tax
        $this->assertIsNumeric($dec_item_report[5]); // Item taxable amount

        $invoice->fresh();
        $invoice->service()->handleCancellation()->save();
        $invoice->save();

        // (new InvoiceTransactionEventEntry())->run($invoice);
        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 2, 1)->startOfDay());

        // Check December report - should show full $30 GST liability
        $payload = [
            'start_date' => '2026-01-01',
            'end_date' => '2026-01-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        // Verify January report shows the cancellation
        $this->assertGreaterThanOrEqual(2, count($data['invoices'])); // At least header + 1 invoice

        // nlog($invoice->fresh()->transaction_events()->get()->toArray());
        // Find our specific invoice in the report
        $found = false;
        $jan_invoice_idx = -1;
        foreach ($data['invoices'] as $idx => $row) {
            if ($idx === 0) continue; // Skip header

            if ((string)$row[0] == (string)$invoice->number) { // Match by invoice number
                $found = true;
                $jan_invoice_idx = $idx;
                break;
            }
        }
        $this->assertTrue($found, 'Invoice not found in Jan');

        // Verify the cancelled invoice row details
        $jan_invoice_report = $data['invoices'][$jan_invoice_idx];

        $this->assertNotNull($jan_invoice_report);
        $this->assertIsString($jan_invoice_report[6]); // Status should be a string
        $this->assertEquals(0, $jan_invoice_report[4]); // Tax reversal (negative)
        $this->assertEquals(0, $jan_invoice_report[5]); // Taxable reversal (negative)

        // Check January report - should show cancelled status
        $payload['start_date'] = '2026-01-01';
        $payload['end_date'] = '2026-01-31';

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        // Find our specific invoice in January report
        $found = false;
        foreach ($data['invoices'] as $idx => $row) {
            if ($idx === 0) continue; // Skip header

            nlog("dafad" . $row[0] . " - " . $invoice->number);

            if ((string)$row[0] == (string)$invoice->number) { // Match by invoice number
                $this->assertIsString($row[6]); // Status should be a string
                $found = true;
                break;
            }
        }
        $this->assertTrue($found, 'Invoice not found in January report 2');

        $this->travelBack();
    }

    /**
     * Test: Invoice with partial payment then cancelled (accrual)
     * Expected: Report taxes on paid portion only
     */
    public function testInvoiceTransactionPaymentHistoryUsesPaymentableCreatedAtDate()
    {
        $this->buildData();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        $this->travelTo(\Carbon\Carbon::create(2025, 10, 15, 9, 30, 0));

        $invoice->service()->applyPaymentAmount(165, 'partial-payment')->save();
        $invoice = $invoice->fresh();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $transaction_event = $invoice->fresh()
            ->transaction_events()
            ->where('event_id', TransactionEvent::INVOICE_UPDATED)
            ->first();

        $this->assertNotNull($transaction_event);

        $payment_history = $transaction_event->metadata->tax_report->payment_history;

        $this->assertCount(1, $payment_history);
        $this->assertEquals('2025-10-15', $payment_history->first()->date);
        $this->assertNotEquals('1970-01-01', $payment_history->first()->date);

        $this->travelBack();
    }

    public function testCancelledInvoiceWithPartialPaymentAccrual()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        // Pay half (165 = 50% of 330)
        $invoice->service()->applyPaymentAmount(165, 'partial-payment')->save();
        $invoice = $invoice->fresh();

        (new InvoiceTransactionEventEntry())->run($invoice);

        // Move to next period and cancel
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());
        $invoice->fresh();
        $invoice->service()->handleCancellation()->save();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        // November report should show cancelled status with 50% of taxes
        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceWithPartialPaymentAccrual', $this->company, $payload, true);

        $this->assertCount(2, $data['invoices']);
        $invoice_report = $data['invoices'][1];

        $this->assertIsString($invoice_report[6]); // Status should be a string
        // TODO: Verify if these values are correct for cancelled invoice with partial payment
        // Current behavior may need review
        $this->assertGreaterThan(0, $invoice_report[4]); // Tax amount should be positive

        $this->travelBack();
    }

    // ========================================
    // DELETED INVOICE TESTS
    // ========================================

    /**
     * Test: Invoice deleted in same period (accrual)
     * Expected: No transaction event created (invoice never became reportable)
     */
    public function testDeletedInvoiceInSamePeriodAccrual()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        // Delete in same period (before transaction event created)
        $invoice->delete();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testDeletedInvoiceInSamePeriodAccrual', $this->company, $payload, true);

        // Should only have header row, no invoice data
        $this->assertCount(1, $data['invoices']); // Just header

        $this->travelBack();
    }

    /**
     * Test: Invoice deleted in next period (accrual)
     * Expected: Original period shows liability, deletion period shows negative reversal
     */
    public function testDeletedInvoiceInNextPeriodAccrual()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $item->tax_name2 = '';
        $item->tax_rate2 = 0;   
        $item->tax_name3 = '';
        $item->tax_rate3 = 0;
        $item->discount = 0;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'public_notes' => 'iamdeleted',
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);


        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 2)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        nlog("initial invoice");

        $data = $this->executeTaxPeriodReportAndSave('testDeletedPaidInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        // Verify October report shows the invoice before deletion
        $this->assertCount(2, $data['invoices']); // Header + 1 invoice
        $this->assertCount(2, $data['invoice_items']); // Header + 1 item

        $oct_invoice_report = $data['invoices'][1];
        $this->assertNotNull($oct_invoice_report);
        $this->assertIsNumeric($oct_invoice_report[2]); // Invoice amount
        $this->assertIsNumeric($oct_invoice_report[3]); // No payment yet
        $this->assertIsNumeric($oct_invoice_report[4]); // Tax amount
        $this->assertIsNumeric($oct_invoice_report[5]); // Taxable amount
        $this->assertIsString($oct_invoice_report[6]); // Status should be a string

        $oct_item_report = $data['invoice_items'][1];
        $this->assertNotNull($oct_item_report);
        $this->assertIsNumeric($oct_item_report[4]); // Item tax
        $this->assertIsNumeric($oct_item_report[5]); // Item taxable amount

        // Move to next period and delete
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());
        $invoice->fresh();
        $repo = new InvoiceRepository();
        $repo->delete($invoice);

        //there would be no trigger for this invoice in a deleted state to have a transaction event entry.

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 2)->startOfDay());

        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

// nlog("post delete");

        $data = $this->executeTaxPeriodReportAndSave('testDeletedPaidInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        // nlog($invoice->fresh()->transaction_events()->get()->toArray());
        // (new InvoiceTransactionEventEntry())->run($invoice);
// nlog($data);

        $this->assertCount(2, $data['invoices']);
        $this->assertEquals(-30, $data['invoices'][1][4]); // +$30 GST


        $data = $this->executeTaxPeriodReportAndSave('testDeletedPaidInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']);
        $this->assertEquals(-30, $data['invoices'][1][4]); // +$30 GST

        // November shows -$30 GST (reversal)
        $payload['start_date'] = '2025-11-01';
        $payload['end_date'] = '2025-11-30';

        $data = $this->executeTaxPeriodReportAndSave('testDeletedPaidInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        nlog($data);

        $this->assertCount(2, $data['invoices']); // Header + 1 deletion row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 deletion row

        $invoice_report = $data['invoices'][1];
        $this->assertNotNull($invoice_report);
        $this->assertIsString($invoice_report[6]); // Status should be a string
        $this->assertIsNumeric($invoice_report[2]); // Negative invoice amount (reversal)
        $this->assertIsNumeric($invoice_report[3]); // No payment
        $this->assertIsNumeric($invoice_report[4]); // Negative GST (reversal)
        $this->assertIsNumeric($invoice_report[5]); // Negative taxable amount (reversal)

        $item_report = $data['invoice_items'][1];
        $this->assertNotNull($item_report);
        $this->assertIsNumeric($item_report[4]); // Negative item tax (reversal)
        $this->assertIsNumeric($item_report[5]); // Negative item taxable (reversal)

        $this->travelBack();
    }

    /**
     * Test: Paid invoice deleted in next period (accrual)
     * Expected: Reversal includes the paid amount
     */
    public function testDeletedPaidInvoiceInNextPeriodAccrual()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'public_notes' => 'iamdeleted',
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->markPaid()->save();
        
        // Move to next period and delete
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testDeletedPaidInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']);

        $repo = new InvoiceRepository();
        $repo->delete($invoice);

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        // November shows deleted with negative amounts
        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testDeletedPaidInvoiceInNextPeriodAccrual', $this->company, $payload, false);

        nlog($data);

        $this->assertCount(2, $data['invoices']); // Header + 1 deletion row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 deletion row

        $invoice_report = $data['invoices'][1];
        $this->assertNotNull($invoice_report);
        $this->assertIsString($invoice_report[6]); // Status should be a string
        $this->assertEquals(-330, $invoice_report[2]); // Negative amount (reversal)
        $this->assertEquals(-330, $invoice_report[3]); // Negative paid_to_date (reversal)
        $this->assertEquals(-30, $invoice_report[4]); // Negative GST (reversal)
        $this->assertEquals(-300, $invoice_report[5]); // Negative taxable amount (reversal)

        $item_report = $data['invoice_items'][1];
        $this->assertNotNull($item_report);
        $this->assertEquals(-30, $item_report[4]); // Negative item tax (reversal)
        $this->assertEquals(-300, $item_report[5]); // Negative item taxable (reversal)

        $this->travelBack();
    }

    // ========================================
    // PAYMENT DELETION TESTS
    // ========================================

    /**
     * Test: Payment deleted in same period as payment (cash accounting)
     * Expected: No net effect on that period
     */
    public function testPaymentDeletedInSamePeriodCash()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'public_notes' => 'iamdeleted',
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->markPaid()->save();

        $payment = $invoice->payments()->first();

        // Delete payment in same period
        $payment->service()->deletePayment();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testPaymentDeletedInSamePeriodCash', $this->company, $payload, true);

        // No payment, no cash report entry
        $this->assertCount(1, $data['invoices']); // Just header

        $this->travelBack();
    }

    /**
     * Test: Payment deleted in next period (cash accounting)
     * Expected: Original period shows +tax, deletion period shows -tax adjustment
     */
    public function testPaymentDeletedInNextPeriodCash()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'public_notes' => 'iamdeleted',
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->markPaid()->save();

        // INVOICE PAID IN OCTOBER

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testPaymentDeletedInNextPeriodCash', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']); // Header + 1 payment row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 payment row

        // Verify October payment row
        $oct_payment_report = $data['invoices'][1];
        $this->assertNotNull($oct_payment_report);
        $this->assertIsNumeric($oct_payment_report[2]); // Invoice amount
        $this->assertIsNumeric($oct_payment_report[3]); // Paid amount
        $this->assertIsNumeric($oct_payment_report[4]); // Tax amount
        $this->assertIsNumeric($oct_payment_report[5]); // Taxable amount
        $this->assertIsString($oct_payment_report[6]); // Status should be a string

        // Verify October payment item row
        $oct_payment_item_report = $data['invoice_items'][1];
        $this->assertNotNull($oct_payment_item_report);
        $this->assertIsNumeric($oct_payment_item_report[4]); // Item tax
        $this->assertIsNumeric($oct_payment_item_report[5]); // Item taxable amount

        //REPORTED IN OCTOBER

        $payment = $invoice->payments()->first();
        $this->assertNotNull($payment);
        // Deleted IN NOVEMBER
        $payment = $payment->service()->deletePayment();

        $this->assertNotNull($payment);

        $this->assertTrue($payment->is_deleted);

        (new \App\Listeners\Payment\PaymentTransactionEventEntry(
            $payment,
            [$invoice->id],
            $this->company->db,
            0,
            true
        ))->handle();
        
        $payment_deleted_event = $invoice->fresh()->transaction_events()->where('event_id', 3)->first();

        $this->assertNotNull($payment_deleted_event);

        nlog($payment_deleted_event->toArray());

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        // October shows +$30 GST (payment received)
        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testPaymentDeletedInNextPeriodCash', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']); // Header + 1 deletion adjustment row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 deletion adjustment row

        // Verify November deletion adjustment row
        $nov_deletion_report = $data['invoices'][1];
        nlog($nov_deletion_report);

        $this->assertNotNull($nov_deletion_report);
        $this->assertEquals(330, $nov_deletion_report[2]); //  amount (reversal)
        $this->assertEquals(0, $nov_deletion_report[3]); //  paid (reversal)
        $this->assertEquals(-30, $nov_deletion_report[4]); // Negative GST (reversal)
        $this->assertEquals(-300, $nov_deletion_report[5]); // Negative taxable (reversal)
        $this->assertIsString($nov_deletion_report[6]); // Status should be a string

        // Verify item deletion adjustment row
        $nov_deletion_item_report = $data['invoice_items'][1];
        $this->assertNotNull($nov_deletion_item_report);
        $this->assertEquals(-30, $nov_deletion_item_report[4]); // Negative item tax (reversal)
        $this->assertEquals(-300, $nov_deletion_item_report[5]); // Negative item taxable (reversal)

        $this->travelBack();
    }

    /**
     * Test: Payment deleted in next period (accrual accounting)
     * Expected: No effect on accrual reports (accrual is based on invoice date, not payment)
     */
    public function testPaymentDeletedInNextPeriodAccrual()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'public_notes' => 'iamdeleted',
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice = $invoice->service()->markSent()->markPaid()->save();

        (new InvoiceTransactionEventEntry())->run($invoice);

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        $payment = $invoice->payments()->first();
        $payment->service()->deletePayment();

        (new PaymentTransactionEventEntry($payment->refresh(), [$invoice->id], $payment->company->db, 0, true))->handle();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        // October accrual report should still show $30 GST (payment deletion doesn't affect accrual)
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true, // accrual
        ];

        $data = $this->executeTaxPeriodReportAndSave('testPaymentDeletedInNextPeriodAccrual', $this->company, $payload, false);

        nlog($invoice->fresh()->transaction_events()->get()->toArray());

        $this->assertCount(2, $data['invoices']); // Header + 1 invoice row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 item row

        // Verify October accrual report (payment deletion doesn't affect accrual)
        $oct_accrual_report = $data['invoices'][1];
        $this->assertNotNull($oct_accrual_report);
        nlog($oct_accrual_report);
        $this->assertEquals(330, $oct_accrual_report[2]); // Invoice amount
        $this->assertEquals(330, $oct_accrual_report[3]); // Paid amount (shows paid even though payment later deleted)
        $this->assertEquals(30, $oct_accrual_report[4]); // Still $30 GST
        $this->assertEquals(300, $oct_accrual_report[5]); // Taxable amount
        $this->assertIsString($oct_accrual_report[6]); // Status should be a string

        // Verify item row
        $oct_accrual_item_report = $data['invoice_items'][1];
        $this->assertNotNull($oct_accrual_item_report);
        $this->assertEquals(30, $oct_accrual_item_report[4]); // Item tax
        $this->assertEquals(300, $oct_accrual_item_report[5]); // Item taxable amount

        // November accrual report should have no entries (payment deletion doesn't create accrual event)
        $payload['start_date'] = '2025-11-01';
        $payload['end_date'] = '2025-11-30';

        $data = $this->executeTaxPeriodReportAndSave('testPaymentDeletedInNextPeriodAccrual', $this->company, $payload, true);

        $this->assertCount(1, $data['invoices']); // Just header, no invoice events in November for accrual

        $this->travelBack();
    }

    // ========================================
    // CANCELLED INVOICE TESTS - CASH ACCOUNTING
    // ========================================

    /**
     * Test: Invoice cancelled in same period (cash accounting)
     * Expected: If unpaid, no transaction event. If paid, need reversal.
     */
    public function testCancelledInvoiceInSamePeriodCash()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        // Cancel in same period (unpaid)
        $invoice->service()->handleCancellation()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceInSamePeriodCash', $this->company, $payload, true);

        // Cash accounting: unpaid cancelled invoice = no tax liability
        $this->assertCount(1, $data['invoices']); // Just header, no data

        $this->travelBack();
    }

    /**
     * Test: Invoice paid then cancelled in same period (cash accounting)
     * Expected: No net effect (payment and cancellation offset)
     */
    public function testCancelledPaidInvoiceInSamePeriodCash()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->markPaid()->save();

        // Cancel after payment in same period
        $invoice->fresh();
        $invoice->service()->handleCancellation()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledPaidInvoiceInSamePeriodCash', $this->company, $payload, true);

        // Should show the payment event but cancellation offsets it
        // The exact behavior depends on implementation
        $this->assertIsArray($data['invoices']);

        $this->travelBack();
    }

    /**
     * Test: Invoice paid in one period, cancelled in next period (cash accounting)
     * Expected: First period shows +tax (payment), second period shows -tax (cancellation reversal)
     *
     * A cancelled partially paid invoice - will not impact future reports. 
     */
    public function testCancelledPartiallyPaidInvoiceInNextPeriodCash()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->applyPaymentAmount(110, 'partial-payment')->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        // Check October report (should show payment)
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledPartiallyPaidInvoiceInNextPeriodCash', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']);
        $this->assertEquals(10, $data['invoices'][1][4]); // +$30 GST from payment

        // Move to next period and cancel
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());
        $invoice->fresh();
        $invoice->service()->handleCancellation()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        // Check November report (should show reversal)
        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledPartiallyPaidInvoiceInNextPeriodCash', $this->company, $payload, false);

        // Should show cancelled status with negative adjustment
        $this->assertCount(1, $data['invoices']);

        $this->travelBack();
    }

    /**
     * Test: Invoice with partial payment then cancelled (cash accounting)
     * Expected: Report taxes only on paid portion, reversal only affects paid amount
     *
     * TODO: Requires cancellation transaction events for cash accounting to be implemented
     */
    public function testCancelledInvoiceWithPartialPaymentCash()
    {

        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        // Pay half (165 = 50% of 330)
        $invoice->service()->applyPaymentAmount(110, 'partial-payment')->save();
        $invoice = $invoice->fresh();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        // Check October report (should show 50% of taxes)
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceWithPartialPaymentCash', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']);
        $this->assertEquals(10, $data['invoices'][1][4]); // +$15 GST (50% of $30)

        // Move to next period and cancel
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());
        $invoice->fresh();
        $invoice->service()->handleCancellation()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        // November report should show reversal of paid portion only
        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCancelledInvoiceWithPartialPaymentCash', $this->company, $payload, false);

        // Should show reversal of the 50% that was paid
        $this->assertEquals(1, count($data['invoices']));

        $this->travelBack();
    }

    // ========================================
    // CREDIT NOTE / REVERSAL TESTS
    // ========================================

    /**
     * Test: Invoice reversed with credit note in next period (accrual)
     * Expected: Original period shows liability, reversal period shows negative adjustment
     *
     * TODO: Implement invoice reversal functionality via credit notes and transaction events
     * This requires creating credits and ensuring they generate appropriate transaction events
     */
    public function testInvoiceReversedWithCreditNoteNextPeriodAccrual()
    {

        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        // Check October report
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true, // accrual
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceReversedWithCreditNoteNextPeriodAccrual', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']);
        $this->assertEquals(30, $data['invoices'][1][4]); // +$30 GST

        // Move to next period and reverse
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());

        $invoice->fresh();
        // $invoice->service()->reverseInvoice()->save();

        $reversal_payload = array_merge($invoice->toArray(), ['invoice_id' => $invoice->hashed_id, 'client_id' => $this->client->hashed_id]);
        $response = $this->withHeaders([
            'X-API-SECRET' => config('ninja.api_secret'),
            'X-API-TOKEN' => $this->_token,
        ])->postJson('/api/v1/credits', $reversal_payload);

        $response->assertStatus(422);

        $invoice = $invoice->fresh();
        $this->assertEquals(Invoice::STATUS_SENT, $invoice->status_id);

        $this->travelBack();
    }

    /**
     * Test: Invoice paid then reversed with credit note (cash accounting)
     * Expected: Payment period shows +tax, reversal period shows -tax
     *
     */
    public function testInvoiceReversedWithCreditNoteNextPeriodCash()
    {

        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => '2025-10-01',
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->markPaid()->save();

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 1)->startOfDay());

        // Check October report (payment received)
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceReversedWithCreditNoteNextPeriodCash', $this->company, $payload, false);

        nlog($data);

        $this->assertCount(2, $data['invoices']); // Header + 1 payment row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 payment row

        // Verify October payment row
        $oct_payment_report = $data['invoices'][1];
        $this->assertNotNull($oct_payment_report);
        $this->assertEquals(330, $oct_payment_report[2]); // Invoice amount
        $this->assertEquals(330, $oct_payment_report[3]); // Paid amount
        $this->assertEquals(30, $oct_payment_report[4]); // +$30 GST
        $this->assertEquals(300, $oct_payment_report[5]); // Taxable amount
        $this->assertIsString($oct_payment_report[6]); // Status should be a string

        // Verify item row
        $oct_payment_item_report = $data['invoice_items'][1];
        $this->assertNotNull($oct_payment_item_report);
        $this->assertEquals(30, $oct_payment_item_report[4]); // Item tax
        $this->assertEquals(300, $oct_payment_item_report[5]); // Item taxable amount

        // Move to next period and reverse
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());

        $invoice->fresh();
        
        $reversal_payload = array_merge($invoice->toArray(), ['invoice_id' => $invoice->hashed_id, 'client_id' => $this->client->hashed_id]);
        $response = $this->withHeaders([
            'X-API-SECRET' => config('ninja.api_secret'),
            'X-API-TOKEN' => $this->_token,
        ])->postJson('/api/v1/credits', $reversal_payload);

        $response->assertStatus(200);

        $credit = \App\Models\Credit::withTrashed()->where('invoice_id', $invoice->id)->first();
        $invoice = $invoice->fresh();

        $this->assertEquals(Invoice::STATUS_REVERSED, $invoice->status_id);

        $this->assertNotNull($credit);

        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        // Check November report (should show reversal)
        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceReversedWithCreditNoteNextPeriodCash', $this->company, $payload, false);

        $reversed_event = $invoice->fresh()->transaction_events()->where('metadata->tax_report->tax_summary->status', 'reversed')->first();
        $this->assertNotNull($reversed_event);

        $this->assertEquals('2025-11-30', $reversed_event->period->format('Y-m-d'));

        // Should show reversal
        $this->assertGreaterThanOrEqual(2, count($data['invoices']));

        $this->travelBack();
    }

    // ========================================
    // COMPLEX MULTI-PERIOD SCENARIOS
    // ========================================

    /**
     * Test: Partial payment, then full refund across different periods
     * Expected: Period 1 shows partial tax, Period 2 shows refund adjustment
     *
     */
    public function testPartialPaymentThenFullRefundAcrossPeriods()
    {
        
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 300;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->save();

        // Pay half in December
        $invoice->service()->applyPaymentAmount(165, 'partial-payment')->save();
        $invoice = $invoice->fresh();

        // Manually trigger the payment cash event listener since it's queued
        $payment = $invoice->payments()->first();
        if ($payment) {
            (new \App\Listeners\Invoice\InvoiceTransactionEventEntryCash())->run($invoice, now()->startOfMonth()->format('Y-m-d'), now()->endOfMonth()->format('Y-m-d'));
        }

        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 1, 1)->startOfDay());

        // Check December (should show 50% of taxes)
        $payload = [
            'start_date' => '2025-12-01',
            'end_date' => '2025-12-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testPartialPaymentThenFullRefundAcrossPeriods', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']); // Header + 1 partial payment row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 partial payment row

        // Verify December partial payment row
        $dec_partial_report = $data['invoices'][1];
        $this->assertNotNull($dec_partial_report);

        nlog($dec_partial_report);

        $this->assertEquals(330, $dec_partial_report[2]); // Partial invoice amount (50%)
        $this->assertEquals(165, $dec_partial_report[3]); // Paid amount (50%)
        $this->assertEquals(15, $dec_partial_report[4]); // +$15 GST (50% of $30)
        $this->assertEquals(150, $dec_partial_report[5]); // +$150 taxable (50% of $300)
        $this->assertIsString($dec_partial_report[6]); // Status should be a string

        // Verify item row
        $dec_partial_item_report = $data['invoice_items'][1];
        $this->assertNotNull($dec_partial_item_report);
        $this->assertEquals(15, $dec_partial_item_report[4]); // Item tax (50%)
        $this->assertEquals(150, $dec_partial_item_report[5]); // Item taxable (50%)

        // Refund the full partial payment in January
        $payment = $invoice->payments()->first();

        $refund_data = [
            'id' => $payment->hashed_id,
            'date' => '2026-01-15',
            'invoices' => [
                [
                    'invoice_id' => $invoice->hashed_id,
                    'amount' => 165, // Full refund of partial payment
                ],
            ]
        ];

        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 1, 15)->startOfDay());

        $response = $this->withHeaders([
            'X-API-SECRET' => config('ninja.api_secret'),
            'X-API-TOKEN' => $this->_token,
        ])->postJson('/api/v1/payments/refund', $refund_data);

        $response->assertStatus(200);

        (new PaymentTransactionEventEntry($payment->refresh(), [$invoice->id], $payment->company->db, 165, false))->handle();

        // Should have: PAYMENT_CASH (from December) + PAYMENT_REFUNDED (from January refund)
        $this->assertEquals(2, $invoice->fresh()->transaction_events()->count());

        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 2, 1)->startOfDay());

        // Check January (should show -$15 reversal)
        $payload = [
            'start_date' => '2026-01-01',
            'end_date' => '2026-01-31',
            'date_range' => 'custom',
            'is_income_billed' => false, // cash
        ];

        $data = $this->executeTaxPeriodReportAndSave('testPartialPaymentThenFullRefundAcrossPeriods', $this->company, $payload, false);

        // Should show negative adjustment
        $this->assertGreaterThanOrEqual(1, count($data['invoices']));

        $found = false;
        foreach ($data['invoices'] as $idx => $row) {
            if ($idx === 0) continue;
            if (isset($row[4]) && $row[4] == -15) {
                $found = true;
                break;
            }
        }

        $this->assertTrue($found, 'Refund adjustment not found in January report');

        $this->travelBack();
    }

    /**
     * Test: Invoice amount increased multiple times across different periods
     * Expected: Each period shows the delta adjustment
     * 
     * Works as expected.
     */
    public function testInvoiceIncreasedMultipleTimesAcrossPeriods()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 10, 1)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 100;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '',
            'tax_rate1' => 0,
            'tax_name2' => '',
            'tax_rate2' => 0,
            'tax_name3' => '',
            'tax_rate3' => 0,
            'custom_surcharge1' => 0,
            'custom_surcharge2' => 0,
            'custom_surcharge3' => 0,
            'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice = $invoice->service()->markSent()->save();

        //100 taxable // 10 tax
        // (new InvoiceTransactionEventEntry())->run($invoice);

        $invoice = $invoice->fresh();
        $this->assertEquals(110, $invoice->fresh()->amount);
        $this->assertEquals(10, $invoice->fresh()->total_taxes);
        $this->assertEquals(2, $invoice->status_id);

        // October: Initial invoice $100 + $10 tax = $110
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 11, 5)->startOfDay());

        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceIncreasedMultipleTimesAcrossPeriods', $this->company, $payload, false);

        $invoice = $invoice->fresh();
        $this->assertEquals(1, $invoice->fresh()->transaction_events()->count());

        // Increase to $200
        $line_items[0]->cost = 200;
        $invoice->line_items = $line_items;
        $invoice = $invoice->calc()->getInvoice();

        $invoice = $invoice->fresh();

        $this->assertEquals(220, $invoice->fresh()->amount);
        $this->assertEquals(20, $invoice->fresh()->total_taxes);
        $this->assertEquals(2, $invoice->status_id);

        // (new InvoiceTransactionEventEntry())->run($invoice);
        // November: Adjustment +$100 + $10 tax
        $this->travelTo(\Carbon\Carbon::createFromDate(2025, 12, 5)->startOfDay());

        $payload = [
            'start_date' => '2025-11-01',
            'end_date' => '2025-11-30',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceIncreasedMultipleTimesAcrossPeriods', $this->company, $payload, false);

        $this->assertEquals(2, $invoice->fresh()->transaction_events()->count());
        
        // Increase to $300
        $line_items[0]->cost = 300;
        $invoice->line_items = $line_items;
        $invoice = $invoice->calc()->getInvoice();

        // (new InvoiceTransactionEventEntry())->run($invoice);

        // December: Adjustment +$100 + $10 tax
        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 1, 5)->startOfDay());

        $payload = [
            'start_date' => '2025-12-01',
            'end_date' => '2025-12-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];
        $data = $this->executeTaxPeriodReportAndSave('testInvoiceIncreasedMultipleTimesAcrossPeriods', $this->company, $payload, false);

        $this->assertEquals(3, $invoice->fresh()->transaction_events()->count());

        nlog($invoice->fresh()->transaction_events()->get()->toArray());
        // Check October
        $payload = [
            'start_date' => '2025-10-01',
            'end_date' => '2025-10-31',
            'date_range' => 'custom',
            'is_income_billed' => true,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceIncreasedMultipleTimesAcrossPeriods', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']); // Header + 1 invoice row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 item row

        // Verify October initial invoice row
        $oct_invoice_report = $data['invoices'][1];
        $this->assertNotNull($oct_invoice_report);
        $this->assertEquals(110, $oct_invoice_report[2]); // $110 amount
        $this->assertEquals(0, $oct_invoice_report[3]); // No payment
        $this->assertEquals(10, $oct_invoice_report[4]); // $10 tax
        $this->assertEquals(100, $oct_invoice_report[5]); // $100 taxable
        $this->assertIsString($oct_invoice_report[6]); // Status should be a string

        // Verify item row
        $oct_item_report = $data['invoice_items'][1];
        $this->assertNotNull($oct_item_report);
        $this->assertEquals(10, $oct_item_report[4]); // $10 tax
        $this->assertEquals(100, $oct_item_report[5]); // $100 taxable

        // Check November
        $payload['start_date'] = '2025-11-01';
        $payload['end_date'] = '2025-11-30';

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceIncreasedMultipleTimesAcrossPeriods', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']); // Header + 1 delta row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 delta row
    
        // Verify November delta row (increase from 100 to 200)
        $nov_delta_report = $data['invoices'][1];
        $this->assertNotNull($nov_delta_report);
        $this->assertEquals(220, $nov_delta_report[2]); // Delta amount (+$100 taxable + $10 tax)
        $this->assertEquals(0, $nov_delta_report[3]); // No payment
        $this->assertEquals(10, $nov_delta_report[4]); // +$10 tax adjustment
        $this->assertEquals(100, $nov_delta_report[5]); // +$100 taxable
        $this->assertIsString($nov_delta_report[6]); // Status should be a string

        // Verify November delta item row
        $nov_delta_item_report = $data['invoice_items'][1];
        $this->assertNotNull($nov_delta_item_report);
        $this->assertEquals(10, $nov_delta_item_report[4]); // Delta tax
        $this->assertEquals(100, $nov_delta_item_report[5]); // Delta taxable

        // Check December
        $payload['start_date'] = '2025-12-01';
        $payload['end_date'] = '2025-12-31';

        $data = $this->executeTaxPeriodReportAndSave('testInvoiceIncreasedMultipleTimesAcrossPeriods', $this->company, $payload, false);

        $this->assertCount(2, $data['invoices']); // Header + 1 delta row
        $this->assertCount(2, $data['invoice_items']); // Header + 1 delta row

        // Verify December delta row (increase from 200 to 300)
        $dec_delta_report = $data['invoices'][1];
        $this->assertNotNull($dec_delta_report);
        $this->assertEquals(330, $dec_delta_report[2]); // Delta amount (+$100 taxable + $10 tax)
        $this->assertEquals(0, $dec_delta_report[3]); // No payment
        $this->assertEquals(10, $dec_delta_report[4]); // +$10 tax adjustment
        $this->assertEquals(100, $dec_delta_report[5]); // +$100 taxable
        $this->assertIsString($dec_delta_report[6]); // Status should be a string

        // Verify December delta item row
        $dec_delta_item_report = $data['invoice_items'][1];
nlog($dec_delta_item_report);

        $this->assertNotNull($dec_delta_item_report);
        $this->assertEquals(10, $dec_delta_item_report[4]); // Delta tax
        $this->assertEquals(100, $dec_delta_item_report[5]); // Delta taxable

        $this->travelBack();
    }

    /**
     * Cash mode: invoice with N payments unwraps to one row per (tax_detail × payment).
     * 0% tax case (Inv41532-shape): 4 payments × 1 tax_detail = 4 rows; column sums equal aggregate.
     */
    public function testCashUnwrapMultiplePaymentsZeroTaxSumsExactly()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 3, 10)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 1434.95;
        $item->tax_name1 = 'Oregon 0%';
        $item->tax_rate1 = 0;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '', 'tax_rate1' => 0,
            'tax_name2' => '', 'tax_rate2' => 0,
            'tax_name3' => '', 'tax_rate3' => 0,
            'custom_surcharge1' => 0, 'custom_surcharge2' => 0,
            'custom_surcharge3' => 0, 'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->createInvitations()->save();

        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(160.00, 'p1')->save();
        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(65.00, 'p2')->save();
        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(300.00, 'p3')->save();
        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(909.95, 'p4')->save();
        $invoice = $invoice->fresh();

        (new InvoiceTransactionEventEntryCash())->run($invoice, '2026-03-01', '2026-03-31');

        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 4, 1)->startOfDay());

        $payload = [
            'start_date' => '2026-03-01',
            'end_date' => '2026-03-31',
            'date_range' => 'custom',
            'is_income_billed' => false,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCashUnwrapMultiplePaymentsZeroTaxSumsExactly', $this->company, $payload, true);

        // Header + 4 cartesian rows (1 tax_detail × 4 payments)
        $this->assertCount(5, $data['invoice_items'], 'Expected header + 4 invoice_item rows for 4 payments');

        $sum_taxable = 0.0;
        $sum_tax = 0.0;
        for ($i = 1; $i < count($data['invoice_items']); $i++) {
            $sum_tax += $data['invoice_items'][$i][4];
            $sum_taxable += $data['invoice_items'][$i][5];
        }
        $this->assertEquals(1434.95, round($sum_taxable, 2), 'Pro-rata taxable_amount must sum to aggregate');
        $this->assertEquals(0.00, round($sum_tax, 2), 'Pro-rata tax_amount must sum to aggregate');

        // Payment columns are appended after base 8 + regional columns. Last 4 cols on each row are payment cols.
        for ($i = 1; $i < count($data['invoice_items']); $i++) {
            $row = $data['invoice_items'][$i];
            $payment_amount_col = count($row) - 2; // payment columns: number, date, amount, refunded
            $payment_number_col = count($row) - 4;
            $this->assertNotEmpty($row[$payment_number_col] ?? null, "Row {$i} should have payment_number column populated");
            $this->assertIsNumeric($row[$payment_amount_col] ?? null, "Row {$i} should have payment_amount column populated");
        }

        $this->travelBack();
    }

    /**
     * Cash mode: pro-rata rounding allocates remainder to the last (date-sorted) payment
     * so column sums equal aggregate to the cent.
     */
    public function testCashUnwrapRoundingRemainderAllocation()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 3, 10)->startOfDay());

        // $100 taxable @ 9% GST = $9 tax. Three payments: $33.33, $33.33, $33.34.
        // Pro-rata: 0.3333 * 9 = 2.9997, 0.3333 * 9 = 2.9997, last absorbs remainder.
        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 100;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 9;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '', 'tax_rate1' => 0,
            'tax_name2' => '', 'tax_rate2' => 0,
            'tax_name3' => '', 'tax_rate3' => 0,
            'custom_surcharge1' => 0, 'custom_surcharge2' => 0,
            'custom_surcharge3' => 0, 'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->createInvitations()->save();
        $this->assertEquals(109, $invoice->amount);

        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(36.33, 'p1')->save();
        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(36.33, 'p2')->save();
        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(36.34, 'p3')->save();
        $invoice = $invoice->fresh();

        (new InvoiceTransactionEventEntryCash())->run($invoice, '2026-03-01', '2026-03-31');

        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 4, 1)->startOfDay());

        $payload = [
            'start_date' => '2026-03-01',
            'end_date' => '2026-03-31',
            'date_range' => 'custom',
            'is_income_billed' => false,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCashUnwrapRoundingRemainderAllocation', $this->company, $payload, true);

        $this->assertCount(4, $data['invoice_items'], 'Expected header + 3 cartesian rows');

        $sum_taxable = 0.0;
        $sum_tax = 0.0;
        for ($i = 1; $i < count($data['invoice_items']); $i++) {
            $sum_tax += $data['invoice_items'][$i][4];
            $sum_taxable += $data['invoice_items'][$i][5];
        }
        // Aggregate must be preserved exactly even when individual rows would round inexactly
        $this->assertEquals(100.00, round($sum_taxable, 2), 'Taxable sum must equal aggregate exactly');
        $this->assertEquals(9.00, round($sum_tax, 2), 'Tax sum must equal aggregate exactly');

        $this->travelBack();
    }

    /**
     * Cash mode: invoice with two distinct tax rates × multiple payments produces
     * cartesian rows. Per-tax-name sums equal that tax's aggregate.
     */
    public function testCashUnwrapMultipleTaxesCartesian()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 3, 10)->startOfDay());

        // Single line item with TWO taxes (GST 10% + STATE 5%) on $100 base = $115 invoice
        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 100;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $item->tax_name2 = 'STATE';
        $item->tax_rate2 = 5;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '', 'tax_rate1' => 0,
            'tax_name2' => '', 'tax_rate2' => 0,
            'tax_name3' => '', 'tax_rate3' => 0,
            'custom_surcharge1' => 0, 'custom_surcharge2' => 0,
            'custom_surcharge3' => 0, 'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->createInvitations()->save();
        $this->assertEquals(115, $invoice->amount);

        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(40, 'p1')->save();
        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(35, 'p2')->save();
        $invoice = $invoice->fresh();
        $invoice->service()->applyPaymentAmount(40, 'p3')->save();
        $invoice = $invoice->fresh();

        (new InvoiceTransactionEventEntryCash())->run($invoice, '2026-03-01', '2026-03-31');

        $event = $invoice->fresh()->transaction_events()
            ->where('event_id', \App\Models\TransactionEvent::PAYMENT_CASH)
            ->first();
        $this->assertCount(2, $event->metadata->tax_report->tax_details, 'Sanity: invoice should have 2 tax_details');

        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 4, 1)->startOfDay());

        $payload = [
            'start_date' => '2026-03-01',
            'end_date' => '2026-03-31',
            'date_range' => 'custom',
            'is_income_billed' => false,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCashUnwrapMultipleTaxesCartesian', $this->company, $payload, true);

        // 2 tax_details × 3 payments = 6 cartesian rows + header
        $this->assertCount(7, $data['invoice_items'], 'Expected header + 6 cartesian rows (2 taxes × 3 payments)');

        // Per-tax aggregation: GST sums to $10, STATE sums to $5
        $by_tax = [];
        for ($i = 1; $i < count($data['invoice_items']); $i++) {
            $row = $data['invoice_items'][$i];
            $name = $row[2];
            $by_tax[$name]['tax'] = ($by_tax[$name]['tax'] ?? 0) + $row[4];
            $by_tax[$name]['taxable'] = ($by_tax[$name]['taxable'] ?? 0) + $row[5];
        }

        // tax_name format from listener metadata may include rate suffix (e.g. "GST 10%")
        $gst_key = collect(array_keys($by_tax))->first(fn ($k) => str_starts_with($k, 'GST'));
        $state_key = collect(array_keys($by_tax))->first(fn ($k) => str_starts_with($k, 'STATE'));
        $this->assertNotNull($gst_key, 'GST tax_detail rows expected');
        $this->assertNotNull($state_key, 'STATE tax_detail rows expected');
        $this->assertEquals(10.00, round($by_tax[$gst_key]['tax'], 2));
        $this->assertEquals(100.00, round($by_tax[$gst_key]['taxable'], 2));
        $this->assertEquals(5.00, round($by_tax[$state_key]['tax'], 2));
        $this->assertEquals(100.00, round($by_tax[$state_key]['taxable'], 2));

        $this->travelBack();
    }

    /**
     * Defensive: a PAYMENT_CASH event with empty payment_history must not divide-by-zero
     * and must fall back to one row per tax_detail.
     */
    public function testCashUnwrapFallbackWhenPaymentHistoryEmpty()
    {
        $this->buildData();
        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 3, 10)->startOfDay());

        $line_items = [];
        $item = InvoiceItemFactory::create();
        $item->quantity = 1;
        $item->cost = 100;
        $item->tax_name1 = 'GST';
        $item->tax_rate1 = 10;
        $line_items[] = $item;

        $invoice = Invoice::factory()->create([
            'client_id' => $this->client->id,
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'line_items' => $line_items,
            'status_id' => Invoice::STATUS_DRAFT,
            'discount' => 0,
            'is_amount_discount' => false,
            'uses_inclusive_taxes' => false,
            'tax_name1' => '', 'tax_rate1' => 0,
            'tax_name2' => '', 'tax_rate2' => 0,
            'tax_name3' => '', 'tax_rate3' => 0,
            'custom_surcharge1' => 0, 'custom_surcharge2' => 0,
            'custom_surcharge3' => 0, 'custom_surcharge4' => 0,
            'date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
        ]);

        $invoice = $invoice->calc()->getInvoice();
        $invoice->service()->markSent()->createInvitations()->save();
        $invoice = $invoice->fresh();

        // Craft a PAYMENT_CASH event with empty payment_history (defensive scenario)
        \App\Models\TransactionEvent::create([
            'company_id' => $invoice->company_id,
            'invoice_id' => $invoice->id,
            'client_id' => $invoice->client_id,
            'client_balance' => $invoice->client->balance,
            'client_paid_to_date' => $invoice->client->paid_to_date,
            'client_credit_balance' => $invoice->client->credit_balance,
            'invoice_balance' => $invoice->balance,
            'invoice_amount' => $invoice->amount,
            'invoice_partial' => $invoice->partial ?? 0,
            'invoice_paid_to_date' => 0,
            'invoice_status' => $invoice->status_id,
            'payment_refunded' => 0,
            'payment_applied' => 0,
            'payment_amount' => 0,
            'event_id' => \App\Models\TransactionEvent::PAYMENT_CASH,
            'timestamp' => now()->timestamp,
            'metadata' => new \App\DataMapper\TransactionEventMetadata([
                'tax_report' => [
                    'tax_details' => [
                        ['tax_name' => 'GST', 'tax_rate' => 10, 'taxable_amount' => 100, 'tax_amount' => 10, 'line_total' => 100, 'total_tax' => 10, 'postal_code' => null],
                    ],
                    'payment_history' => [],
                    'tax_summary' => ['taxable_amount' => 100, 'tax_amount' => 10, 'status' => 'updated'],
                ],
            ]),
            'period' => '2026-03-31',
        ]);

        $this->travelTo(\Carbon\Carbon::createFromDate(2026, 4, 1)->startOfDay());

        $payload = [
            'start_date' => '2026-03-01',
            'end_date' => '2026-03-31',
            'date_range' => 'custom',
            'is_income_billed' => false,
        ];

        $data = $this->executeTaxPeriodReportAndSave('testCashUnwrapFallbackWhenPaymentHistoryEmpty', $this->company, $payload, true);

        // Header + 1 tax_detail row (no per-payment unwrap, no /0)
        $this->assertCount(2, $data['invoice_items'], 'Empty payment_history must fall back to one row per tax_detail');

        $row = $data['invoice_items'][1];
        $this->assertEquals(10, $row[4], 'Fallback row carries the full tax_amount');
        $this->assertEquals(100, $row[5], 'Fallback row carries the full taxable_amount');

        $this->travelBack();
    }
}
