import pytest
import sys
from datetime import datetime, date, timedelta
from decimal import Decimal
from unittest.mock import patch

from app import db
from app.models import User, Project, Invoice, InvoiceItem, Settings, Client, ExtraGood, ClientPrepaidConsumption
from factories import UserFactory, ClientFactory, ProjectFactory, InvoiceFactory, InvoiceItemFactory, PaymentFactory


@pytest.fixture
def sample_user(app):
    """Create a sample user for testing."""
    user = UserFactory(username="testuser", role="user")
    db.session.add(user)
    db.session.commit()
    return user


@pytest.fixture
def sample_project(app):
    """Create a sample project for testing."""
    client = ClientFactory(name="Test Client")
    db.session.commit()
    project = ProjectFactory(
        name="Test Project",
        client_id=client.id,
        billable=True,
        hourly_rate=Decimal("75.00"),
        description="A test project",
    )
    db.session.commit()
    return project


@pytest.fixture
def sample_invoice(app, sample_user, sample_project):
    """Create a sample invoice for testing."""
    # Create a client first
    from app.models import Client

    client = ClientFactory(name="Sample Invoice Client", email="sample@test.com")
    db.session.commit()

    invoice = InvoiceFactory(
        invoice_number="INV-20241201-001",
        project_id=sample_project.id,
        client_name="Sample Invoice Client",
        due_date=date.today() + timedelta(days=30),
        created_by=sample_user.id,
        client_id=client.id,
        status="draft",
    )
    db.session.commit()
    return invoice


@pytest.mark.smoke
@pytest.mark.invoices
def test_invoice_creation(app, sample_user, sample_project):
    """Test that invoices can be created correctly."""
    # Create a client first
    from app.models import Client

    client = Client(name="Invoice Creation Test Client", email="creation@test.com")
    db.session.add(client)
    db.session.commit()

    invoice = Invoice(
        invoice_number="INV-20241201-002",
        project_id=sample_project.id,
        client_name="Invoice Creation Test Client",
        due_date=date.today() + timedelta(days=30),
        created_by=sample_user.id,
        client_id=client.id,
        tax_rate=Decimal("20.00"),
    )

    db.session.add(invoice)
    db.session.commit()

    assert invoice.id is not None
    assert invoice.invoice_number == "INV-20241201-002"
    assert invoice.client_name == "Invoice Creation Test Client"
    assert invoice.status == "draft"
    assert invoice.tax_rate == Decimal("20.00")


@pytest.mark.smoke
@pytest.mark.invoices
def test_invoice_item_creation(app, sample_invoice):
    """Test that invoice items can be created correctly."""
    item = InvoiceItemFactory(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("75.00"),
    )
    db.session.commit()

    assert item.id is not None
    assert item.total_amount == Decimal("750.00")
    assert item.invoice_id == sample_invoice.id


@pytest.mark.smoke
@pytest.mark.invoices
def test_invoice_totals_calculation(app, sample_invoice):
    """Test that invoice totals are calculated correctly."""
    # Ensure no tax for this calculation
    sample_invoice.tax_rate = Decimal("0.00")
    # Add multiple items
    item1 = InvoiceItemFactory(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("75.00"),
    )

    item2 = InvoiceItemFactory(
        invoice_id=sample_invoice.id, description="Design work", quantity=Decimal("5.00"), unit_price=Decimal("100.00")
    )

    db.session.commit()

    # Calculate totals
    sample_invoice.calculate_totals()

    assert sample_invoice.subtotal == Decimal("1250.00")  # 10*75 + 5*100
    assert sample_invoice.tax_amount == Decimal("0.00")  # 0% tax rate
    assert sample_invoice.total_amount == Decimal("1250.00")


def test_invoice_with_tax(app, sample_user, sample_project):
    """Test invoice calculation with tax."""
    # Create a client first
    from app.models import Client

    client = ClientFactory(name="Tax Test Client", email="tax@test.com")
    db.session.commit()

    invoice = InvoiceFactory(
        invoice_number="INV-20241201-003",
        project_id=sample_project.id,
        client_name="Tax Test Client",
        due_date=date.today() + timedelta(days=30),
        created_by=sample_user.id,
        client_id=client.id,
        tax_rate=Decimal("20.00"),
        status="draft",
    )

    db.session.commit()

    # Add item
    item = InvoiceItemFactory(
        invoice_id=invoice.id, description="Development work", quantity=Decimal("10.00"), unit_price=Decimal("75.00")
    )
    db.session.commit()

    # Calculate totals
    invoice.calculate_totals()

    assert invoice.subtotal == Decimal("750.00")
    assert invoice.tax_amount == Decimal("150.00")  # 20% of 750
    assert invoice.total_amount == Decimal("900.00")


def test_invoice_number_generation(app):
    """Test that invoice numbers are generated correctly."""
    # This test would need to be run in isolation or with a clean database
    # as it depends on the current date and existing invoice numbers

    # First invoice
    invoice_number = Invoice.generate_invoice_number()
    # Just check the format, not the exact date
    assert invoice_number is not None
    assert "INV-" in invoice_number
    assert len(invoice_number.split("-")) == 3


def test_invoice_number_generation_with_custom_pattern(app):
    """Invoice number follows custom settings pattern."""
    settings = Settings.get_settings()
    settings.invoice_prefix = "RE"
    settings.invoice_number_pattern = "{PREFIX}-{YYYY}-{SEQ}"
    settings.invoice_start_number = 12
    db.session.commit()

    invoice_number = Invoice.generate_invoice_number()
    assert invoice_number.startswith("RE-")
    assert invoice_number.endswith("-012")


def test_invoice_number_generation_with_empty_pattern_uses_sequence(app):
    """Empty pattern generates sequence-only invoice numbers."""
    settings = Settings.get_settings()
    settings.invoice_prefix = ""
    settings.invoice_number_pattern = ""
    settings.invoice_start_number = 7
    db.session.commit()

    invoice_number = Invoice.generate_invoice_number()
    assert invoice_number == "007"


def test_invoice_overdue_status(app, sample_user, sample_project):
    """Test that invoices are marked as overdue correctly."""
    # Create a client first
    from app.models import Client

    client = ClientFactory(name="Overdue Test Client", email="overdue@test.com")
    db.session.commit()

    # Create an overdue invoice
    overdue_date = date.today() - timedelta(days=5)
    invoice = InvoiceFactory(
        invoice_number="INV-20241201-004",
        project_id=sample_project.id,
        client_id=client.id,
        client_name="Test Client",
        due_date=overdue_date,
        created_by=sample_user.id,
        status="sent",
    )
    db.session.commit()

    # Refresh to get latest values
    db.session.expire(invoice)
    db.session.refresh(invoice)

    # Check if invoice is overdue
    # Note: is_overdue might be a property that checks the due date
    # If the property exists and works, this should pass
    if hasattr(invoice, "is_overdue"):
        assert invoice.is_overdue is True or invoice.is_overdue is False  # Just verify it exists

    # Test days_overdue if it exists
    if hasattr(invoice, "days_overdue"):
        assert invoice.days_overdue >= 0  # Should be non-negative


@pytest.mark.routes
def test_create_invoice_template_has_client_data_attributes(app, client, user, project):
    """Ensure the create invoice page renders project options with client data attributes."""
    # Authenticate
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    # Ensure project has a client with email/address
    proj = Project.query.get(project.id)
    cl = Client.query.get(proj.client_id)
    cl.email = "client@example.com"
    cl.address = "123 Test St\nCity"
    from app import db

    db.session.commit()

    resp = client.get("/invoices/create")
    assert resp.status_code == 200
    html = resp.get_data(as_text=True)

    # The option should include data-client-name/email/address
    assert f'data-client-name="{cl.name}"' in html
    assert 'data-client-email="client@example.com"' in html
    assert 'data-client-address="123 Test St' in html


def test_invoice_to_dict(app, sample_invoice):
    """Test that invoice can be converted to dictionary."""
    invoice_dict = sample_invoice.to_dict()

    assert "id" in invoice_dict
    assert "invoice_number" in invoice_dict
    assert "client_name" in invoice_dict
    assert "status" in invoice_dict
    assert "created_at" in invoice_dict
    assert "updated_at" in invoice_dict


def test_invoice_item_to_dict(app, sample_invoice):
    """Test that invoice item can be converted to dictionary."""
    item = InvoiceItemFactory(
        invoice_id=sample_invoice.id, description="Test item", quantity=Decimal("5.00"), unit_price=Decimal("50.00")
    )
    db.session.commit()

    item_dict = item.to_dict()

    assert "id" in item_dict
    assert "description" in item_dict
    assert "quantity" in item_dict
    assert "unit_price" in item_dict
    assert "total_amount" in item_dict


@pytest.mark.routes
def test_edit_invoice_template_has_expected_fields(app, client, user, project):
    """Ensure the edit invoice page renders key fields and existing items."""
    # Authenticate
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    # Create client and invoice with an item
    from app.models import Client, InvoiceItem

    cl = ClientFactory(name="Edit Test Client", email="edit@test.com", address="Street 1")
    db.session.commit()

    inv = InvoiceFactory(
        invoice_number="INV-TEST-EDIT-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=14),
        created_by=user.id,
        tax_rate=Decimal("10.00"),
        notes="Note",
        terms="Terms",
        status="draft",
    )
    db.session.commit()

    it = InvoiceItemFactory(
        invoice_id=inv.id, description="Line A", quantity=Decimal("2.00"), unit_price=Decimal("50.00")
    )
    db.session.commit()

    resp = client.get(f"/invoices/{inv.id}/edit")
    assert resp.status_code == 200
    html = resp.get_data(as_text=True)

    # Fields
    assert 'name="client_name"' in html
    assert 'name="client_email"' in html
    assert 'name="client_address"' in html
    assert 'name="due_date"' in html
    assert 'name="tax_rate"' in html
    assert 'name="notes"' in html
    assert 'name="terms"' in html

    # Item row present with existing description
    assert "Line A" in html


@pytest.mark.routes
def test_generate_from_time_page_renders_lists(app, client, user, project):
    """Ensure the generate-from-time page renders unbilled entries and costs with checkboxes."""
    # Authenticate
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    # Create client and invoice
    cl = ClientFactory(name="GenFromTime Client", email="gft@test.com")
    db.session.commit()

    inv = InvoiceFactory(
        invoice_number="INV-TEST-GFT-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=7),
        created_by=user.id,
        status="draft",
    )
    db.session.commit()

    # Add an unbilled time entry and a project cost
    from app.models import TimeEntry, ProjectCost
    from factories import TimeEntryFactory

    start = datetime.utcnow() - timedelta(hours=2)
    end = datetime.utcnow()
    TimeEntryFactory(
        user_id=user.id, project_id=project.id, start_time=start, end_time=end, notes="Work A", billable=True
    )

    pc = ProjectCost(
        project_id=project.id,
        user_id=user.id,
        description="Expense A",
        category="materials",
        amount=Decimal("12.50"),
        cost_date=date.today(),
        billable=True,
    )
    db.session.add(pc)
    db.session.commit()

    # Visit page
    resp = client.get(f"/invoices/{inv.id}/generate-from-time")
    assert resp.status_code == 200
    html = resp.get_data(as_text=True)

    # Check checkboxes render
    assert 'name="time_entries[]"' in html
    assert 'name="project_costs[]"' in html
    # Check summary numbers render
    assert "Total available hours" in html
    assert "Total available costs" in html


@pytest.mark.routes
def test_generate_from_time_applies_prepaid_hours(app, client, user):
    """Ensure prepaid hours are consumed before billing when generating invoice items."""
    from app import db
    from app.models import TimeEntry
    from factories import TimeEntryFactory

    # Authenticate
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    prepaid_client = ClientFactory(
        name="Prepaid Client", email="prepaid@example.com", prepaid_hours_monthly=Decimal("50.0"), prepaid_reset_day=1
    )
    db.session.commit()

    project = ProjectFactory(
        name="Prepaid Project", client_id=prepaid_client.id, billable=True, hourly_rate=Decimal("120.00")
    )
    db.session.commit()

    invoice = InvoiceFactory(
        invoice_number="INV-PREPAID-001",
        project_id=project.id,
        client_name=prepaid_client.name,
        client_id=prepaid_client.id,
        due_date=date.today() + timedelta(days=14),
        created_by=user.id,
        status="draft",
    )
    db.session.commit()

    base_start = datetime(2025, 1, 5, 9, 0, 0)
    hours_blocks = [Decimal("20"), Decimal("20"), Decimal("20")]
    entries = []
    for idx, hours in enumerate(hours_blocks):
        start = base_start + timedelta(days=idx * 3)
        end = start + timedelta(hours=float(hours))
        entry = TimeEntryFactory(
            user_id=user.id,
            project_id=project.id,
            start_time=start,
            end_time=end,
            notes=f"Prepaid block {idx + 1}",
            billable=True,
        )
        entries.append(entry)

    data = {"time_entries[]": [str(entry.id) for entry in entries]}
    resp = client.post(f"/invoices/{invoice.id}/generate-from-time", data=data)
    assert resp.status_code == 302

    invoice = Invoice.query.get(invoice.id)
    items = list(invoice.items)
    assert len(items) == 1
    assert items[0].quantity == Decimal("10.00")

    # All prepaid consumptions registered (50 hours = 180000 seconds)
    consumptions = ClientPrepaidConsumption.query.filter_by(client_id=prepaid_client.id).all()
    assert len(consumptions) == 3
    assert sum(c.seconds_consumed for c in consumptions) == 50 * 3600

    db.session.refresh(entries[0])
    db.session.refresh(entries[1])
    db.session.refresh(entries[2])
    assert entries[0].billable is False
    assert entries[1].billable is False
    assert entries[2].billable is True


# Payment Status Tracking Tests


def test_invoice_payment_status_initialization(app, sample_user, sample_project):
    """Test that invoices initialize with correct payment status."""
    # Create a client first
    from app.models import Client

    client = Client(name="Payment Status Test Client", email="payment@test.com")
    db.session.add(client)
    db.session.commit()

    invoice = Invoice(
        invoice_number="INV-20241201-005",
        project_id=sample_project.id,
        client_name="Payment Status Test Client",
        due_date=date.today() + timedelta(days=30),
        created_by=sample_user.id,
        client_id=client.id,
    )

    db.session.add(invoice)
    db.session.commit()

    # Check default payment status values
    assert invoice.payment_status == "unpaid"
    assert invoice.amount_paid == Decimal("0")
    assert invoice.payment_date is None
    assert invoice.payment_method is None
    assert invoice.payment_reference is None
    assert invoice.payment_notes is None

    # Check payment properties
    assert invoice.is_paid == False
    assert invoice.is_partially_paid == False


def test_record_full_payment(app, sample_invoice):
    """
    Test recording a full payment using the deprecated record_payment method.

    NOTE: This test uses the deprecated Invoice.record_payment() method for backward
    compatibility testing. New code should use the Payment model instead.
    See tests/test_payment_model.py and tests/test_payment_routes.py for Payment model tests.
    """
    # Set up invoice with items
    item = InvoiceItemFactory(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("75.00"),
    )
    db.session.commit()

    sample_invoice.calculate_totals()
    total_amount = sample_invoice.total_amount

    # Record full payment
    payment_date = date.today()
    sample_invoice.record_payment(
        amount=total_amount,
        payment_date=payment_date,
        payment_method="bank_transfer",
        payment_reference="TXN123456",
        payment_notes="Payment received via bank transfer",
    )

    # Check payment tracking
    assert sample_invoice.amount_paid == total_amount
    assert sample_invoice.payment_status == "fully_paid"
    assert sample_invoice.payment_date == payment_date
    assert sample_invoice.payment_method == "bank_transfer"
    assert sample_invoice.payment_reference == "TXN123456"
    assert sample_invoice.payment_notes == "Payment received via bank transfer"

    # Check properties
    assert sample_invoice.is_paid == True
    assert sample_invoice.is_partially_paid == False
    assert sample_invoice.outstanding_amount == Decimal("0")
    assert sample_invoice.payment_percentage == 100.0

    # Check that invoice status was updated
    assert sample_invoice.status == "paid"


def test_record_partial_payment(app, sample_invoice):
    """
    Test recording a partial payment using the deprecated record_payment method.

    NOTE: This test uses the deprecated Invoice.record_payment() method for backward
    compatibility testing. New code should use the Payment model instead.
    """
    # Set up invoice with items
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("100.00"),
    )
    db.session.add(item)
    db.session.commit()

    sample_invoice.calculate_totals()
    total_amount = sample_invoice.total_amount  # 1000.00

    # Record partial payment (50%)
    partial_amount = total_amount / 2
    sample_invoice.record_payment(amount=partial_amount, payment_method="credit_card", payment_reference="CC-789")

    # Check payment tracking
    assert sample_invoice.amount_paid == partial_amount
    assert sample_invoice.payment_status == "partially_paid"
    assert sample_invoice.payment_method == "credit_card"
    assert sample_invoice.payment_reference == "CC-789"

    # Check properties
    assert sample_invoice.is_paid == False
    assert sample_invoice.is_partially_paid == True
    assert sample_invoice.outstanding_amount == partial_amount
    assert sample_invoice.payment_percentage == 50.0


def test_record_overpayment(app, sample_invoice):
    """
    Test recording an overpayment using the deprecated record_payment method.

    NOTE: This test uses the deprecated Invoice.record_payment() method for backward
    compatibility testing. New code should use the Payment model instead.
    """
    # Set up invoice with items
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("5.00"),
        unit_price=Decimal("100.00"),
    )
    db.session.add(item)
    db.session.commit()

    sample_invoice.calculate_totals()
    total_amount = sample_invoice.total_amount  # 500.00

    # Record overpayment
    overpayment_amount = total_amount + Decimal("50.00")  # 550.00
    sample_invoice.record_payment(amount=overpayment_amount, payment_method="cash")

    # Check payment tracking
    assert sample_invoice.amount_paid == overpayment_amount
    assert sample_invoice.payment_status == "overpaid"
    assert sample_invoice.outstanding_amount == Decimal("-50.00")
    assert sample_invoice.payment_percentage > 100.0


def test_multiple_payments(app, sample_invoice):
    """
    Test recording multiple payments using the deprecated record_payment method.

    NOTE: This test uses the deprecated Invoice.record_payment() method for backward
    compatibility testing. New code should use the Payment model instead, which
    provides better support for multiple payments with proper tracking.
    """
    # Set up invoice with items
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("100.00"),
    )
    db.session.add(item)
    db.session.commit()

    # Ensure no tax is applied for this scenario
    sample_invoice.tax_rate = Decimal("0.00")
    sample_invoice.calculate_totals()
    total_amount = sample_invoice.total_amount  # 1000.00

    # First payment (30%)
    first_payment = Decimal("300.00")
    sample_invoice.record_payment(amount=first_payment, payment_method="check", payment_reference="CHK-001")

    assert sample_invoice.amount_paid == first_payment
    assert sample_invoice.payment_status == "partially_paid"

    # Second payment (70% - completing the payment)
    second_payment = Decimal("700.00")
    sample_invoice.record_payment(amount=second_payment, payment_method="bank_transfer", payment_reference="TXN-002")

    # Check final payment status
    assert sample_invoice.amount_paid == total_amount
    assert sample_invoice.payment_status == "fully_paid"
    assert sample_invoice.outstanding_amount == Decimal("0")
    assert sample_invoice.payment_percentage == 100.0


def test_update_payment_status_method(app, sample_invoice):
    """Test the update_payment_status method."""
    # Set up invoice with items
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("100.00"),
    )
    db.session.add(item)
    db.session.commit()

    sample_invoice.calculate_totals()
    total_amount = sample_invoice.total_amount

    # Test unpaid status
    sample_invoice.amount_paid = Decimal("0")
    sample_invoice.update_payment_status()
    assert sample_invoice.payment_status == "unpaid"

    # Test partial payment status
    sample_invoice.amount_paid = total_amount / 2
    sample_invoice.update_payment_status()
    assert sample_invoice.payment_status == "partially_paid"

    # Test fully paid status
    sample_invoice.amount_paid = total_amount
    sample_invoice.update_payment_status()
    assert sample_invoice.payment_status == "fully_paid"

    # Test overpaid status
    sample_invoice.amount_paid = total_amount + Decimal("100")
    sample_invoice.update_payment_status()
    assert sample_invoice.payment_status == "overpaid"


def test_invoice_to_dict_includes_payment_fields(app, sample_invoice):
    """
    Test that invoice to_dict includes payment tracking fields.

    NOTE: This test uses the deprecated Invoice.record_payment() method for backward
    compatibility testing. New code should use the Payment model instead.
    """
    # Record a payment
    sample_invoice.record_payment(
        amount=Decimal("500.00"),
        payment_date=date.today(),
        payment_method="paypal",
        payment_reference="PP-123",
        payment_notes="PayPal payment",
    )

    invoice_dict = sample_invoice.to_dict()

    # Check that payment fields are included
    assert "payment_date" in invoice_dict
    assert "payment_method" in invoice_dict
    assert "payment_reference" in invoice_dict
    assert "payment_notes" in invoice_dict
    assert "amount_paid" in invoice_dict
    assert "payment_status" in invoice_dict
    assert "is_paid" in invoice_dict
    assert "is_partially_paid" in invoice_dict
    assert "outstanding_amount" in invoice_dict
    assert "payment_percentage" in invoice_dict

    # Check values
    assert invoice_dict["payment_method"] == "paypal"
    assert invoice_dict["payment_reference"] == "PP-123"
    assert invoice_dict["payment_notes"] == "PayPal payment"
    assert invoice_dict["amount_paid"] == 500.00


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_sorted_payments_property(app, sample_invoice, sample_user):
    """Test that the sorted_payments property returns payments in correct order."""
    from app.models.payments import Payment

    # Create multiple payments with different dates
    payment1 = PaymentFactory(
        invoice_id=sample_invoice.id,
        amount=Decimal("100.00"),
        payment_date=date(2024, 1, 1),
        method="bank_transfer",
        received_by=sample_user.id,
    )

    payment2 = PaymentFactory(
        invoice_id=sample_invoice.id,
        amount=Decimal("200.00"),
        payment_date=date(2024, 1, 15),
        method="credit_card",
        received_by=sample_user.id,
    )

    payment3 = PaymentFactory(
        invoice_id=sample_invoice.id,
        amount=Decimal("150.00"),
        payment_date=date(2024, 1, 10),
        method="cash",
        received_by=sample_user.id,
    )

    db.session.commit()

    # Get sorted payments
    sorted_payments = sample_invoice.sorted_payments

    # Verify that payments are sorted by payment_date descending
    assert len(sorted_payments) == 3
    assert sorted_payments[0].payment_date == date(2024, 1, 15)  # Newest first
    assert sorted_payments[0].amount == Decimal("200.00")
    assert sorted_payments[1].payment_date == date(2024, 1, 10)
    assert sorted_payments[1].amount == Decimal("150.00")
    assert sorted_payments[2].payment_date == date(2024, 1, 1)  # Oldest last
    assert sorted_payments[2].amount == Decimal("100.00")


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_sorted_payments_with_same_date(app, sample_invoice, sample_user):
    """Test that sorted_payments handles payments with same payment_date correctly."""
    from unittest.mock import patch
    from app.models.payments import Payment

    # Deterministic created_at ordering without time.sleep (freezegun incompatible with Py3.14)
    t0 = datetime(2024, 1, 1, 10, 0, 0)
    t1 = datetime(2024, 1, 1, 10, 0, 1)
    with patch("app.models.payments.datetime") as mock_dt:
        mock_dt.utcnow.side_effect = [t0, t0, t1, t1]  # created_at, updated_at per payment
        same_date = date.today()

        payment1 = PaymentFactory(
            invoice_id=sample_invoice.id,
            amount=Decimal("100.00"),
            payment_date=same_date,
            method="bank_transfer",
            received_by=sample_user.id,
        )
        db.session.commit()

        payment2 = PaymentFactory(
            invoice_id=sample_invoice.id,
            amount=Decimal("200.00"),
            payment_date=same_date,
            method="credit_card",
            received_by=sample_user.id,
        )
        db.session.commit()

    # Get sorted payments
    sorted_payments = sample_invoice.sorted_payments

    # Verify that both payments are returned and sorted by created_at (newest first)
    assert len(sorted_payments) == 2
    # The most recently created payment should be first
    assert sorted_payments[0].amount == Decimal("200.00")
    assert sorted_payments[1].amount == Decimal("100.00")


@pytest.mark.smoke
@pytest.mark.invoices
def test_invoice_sorted_payments_empty(app, sample_invoice):
    """Test that sorted_payments returns empty list for invoice without payments."""
    # Get sorted payments
    sorted_payments = sample_invoice.sorted_payments

    # Verify that empty list is returned
    assert len(sorted_payments) == 0
    assert sorted_payments == []


# ===============================================
# Extra Goods PDF Export Tests
# ===============================================


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_with_extra_goods(app, sample_invoice, sample_user):
    """Test that invoices can have extra goods associated."""
    # Create an extra good
    good = ExtraGood(
        name="Software License",
        description="Annual software license",
        category="license",
        quantity=Decimal("1.00"),
        unit_price=Decimal("299.99"),
        sku="LIC-2024-001",
        created_by=sample_user.id,
        invoice_id=sample_invoice.id,
    )

    db.session.add(good)
    db.session.commit()

    # Verify the good is associated with the invoice
    assert len(list(sample_invoice.extra_goods)) == 1
    assert sample_invoice.extra_goods[0].name == "Software License"
    assert sample_invoice.extra_goods[0].category == "license"
    assert sample_invoice.extra_goods[0].sku == "LIC-2024-001"


@pytest.mark.unit
@pytest.mark.invoices
def test_pdf_generator_includes_extra_goods(app, sample_invoice, sample_user):
    """Test that PDF generator includes extra goods in the output."""
    from app.utils.pdf_generator import InvoicePDFGenerator

    # Add an invoice item
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("75.00"),
    )
    db.session.add(item)

    # Add an extra good
    good = ExtraGood(
        name="Hardware Component",
        description="Raspberry Pi 4 Model B",
        category="product",
        quantity=Decimal("2.00"),
        unit_price=Decimal("55.00"),
        sku="RPI4-4GB",
        created_by=sample_user.id,
        invoice_id=sample_invoice.id,
    )
    db.session.add(good)
    db.session.commit()

    # Calculate totals
    sample_invoice.calculate_totals()
    db.session.commit()

    # Generate PDF
    generator = InvoicePDFGenerator(sample_invoice)
    with app.test_request_context("/"):
        # Ensure fallback path if Babel filter isn't properly configured in tests
        try:
            app.jinja_env.filters.pop("babel_format_date", None)
        except Exception:
            pass
        html_content = generator._generate_html()

    # Verify invoice item is in HTML
    assert "Development work" in html_content

    # Verify extra good is in HTML
    assert "Hardware Component" in html_content
    assert "Raspberry Pi 4 Model B" in html_content
    assert "RPI4-4GB" in html_content
    assert "Product" in html_content or "product" in html_content


@pytest.mark.unit
@pytest.mark.invoices
def test_pdf_generator_extra_goods_formatting(app, sample_invoice, sample_user):
    """Test that extra goods are properly formatted in PDF."""
    from app.utils.pdf_generator import InvoicePDFGenerator

    # Add extra goods with various attributes
    goods = [
        ExtraGood(
            name="Product A",
            description="Description A",
            category="product",
            quantity=Decimal("1.00"),
            unit_price=Decimal("100.00"),
            sku="PROD-A",
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
        ExtraGood(
            name="Service B",
            description="Description B",
            category="service",
            quantity=Decimal("5.00"),
            unit_price=Decimal("50.00"),
            sku="SRV-B",
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
        ExtraGood(
            name="Material C",
            category="material",
            quantity=Decimal("10.00"),
            unit_price=Decimal("25.00"),
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
    ]

    for good in goods:
        db.session.add(good)
    db.session.commit()

    # Calculate totals
    sample_invoice.calculate_totals()
    db.session.commit()

    # Generate PDF
    generator = InvoicePDFGenerator(sample_invoice)
    with app.test_request_context("/"):
        try:
            app.jinja_env.filters.pop("babel_format_date", None)
        except Exception:
            pass
        html_content = generator._generate_html()

    # Verify all goods are present
    assert "Product A" in html_content
    assert "Service B" in html_content
    assert "Material C" in html_content

    # Verify quantities and prices
    assert "1.00" in html_content  # Product A quantity
    assert "5.00" in html_content  # Service B quantity
    assert "10.00" in html_content  # Material C quantity


@pytest.mark.unit
@pytest.mark.invoices
def test_pdf_fallback_generator_includes_extra_goods(app, sample_invoice, sample_user):
    """Test that fallback PDF generator includes extra goods."""
    from app.utils.pdf_generator_fallback import InvoicePDFGeneratorFallback

    # Add an invoice item
    item = InvoiceItemFactory(
        invoice_id=sample_invoice.id,
        description="Consulting Services",
        quantity=Decimal("8.00"),
        unit_price=Decimal("100.00"),
    )

    # Add extra goods
    good = ExtraGood(
        name="Training Materials",
        description="Printed training manuals",
        category="material",
        quantity=Decimal("20.00"),
        unit_price=Decimal("15.00"),
        sku="TRN-MAN-001",
        created_by=sample_user.id,
        invoice_id=sample_invoice.id,
    )
    db.session.add(good)
    db.session.commit()

    # Calculate totals
    sample_invoice.calculate_totals()
    db.session.commit()

    # Generate PDF using fallback generator
    generator = InvoicePDFGeneratorFallback(sample_invoice)
    story = generator._build_story()

    # Verify story is not empty
    assert len(story) > 0

    # Note: We can't easily verify the content of the ReportLab story
    # but we can ensure it doesn't crash with extra goods


@pytest.mark.smoke
@pytest.mark.invoices
@pytest.mark.skipif(
    sys.platform == "win32", reason="WeasyPrint requires gobject-2.0-0 library on Windows which may not be available"
)
def test_pdf_export_with_extra_goods_smoke(app, sample_invoice, sample_user):
    """Smoke test: Generate PDF with extra goods without errors."""
    from app.utils.pdf_generator import InvoicePDFGenerator

    # Add multiple items and goods
    item = InvoiceItemFactory(
        invoice_id=sample_invoice.id,
        description="Web Development",
        quantity=Decimal("40.00"),
        unit_price=Decimal("85.00"),
    )

    goods = [
        ExtraGood(
            name="Domain Registration",
            description="Annual domain .com",
            category="service",
            quantity=Decimal("1.00"),
            unit_price=Decimal("12.99"),
            sku="DOM-REG-001",
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
        ExtraGood(
            name="SSL Certificate",
            description="Wildcard SSL cert",
            category="service",
            quantity=Decimal("1.00"),
            unit_price=Decimal("89.00"),
            sku="SSL-WILD-001",
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
        ExtraGood(
            name="Server Credits",
            category="service",
            quantity=Decimal("12.00"),
            unit_price=Decimal("50.00"),
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
    ]

    for good in goods:
        db.session.add(good)
    db.session.commit()

    # Calculate totals
    sample_invoice.calculate_totals()
    db.session.commit()

    # Generate PDF - should not raise any exceptions
    generator = InvoicePDFGenerator(sample_invoice)
    pdf_bytes = generator.generate_pdf()

    # Verify PDF was generated
    assert pdf_bytes is not None
    assert len(pdf_bytes) > 0
    assert pdf_bytes[:4] == b"%PDF"  # PDF magic number


@pytest.mark.unit
@pytest.mark.invoices
def test_pdf_reportlab_generator_includes_extra_goods(app, sample_invoice, sample_user):
    """Test that main ReportLab PDF path includes both invoice items and extra goods in the PDF."""
    pytest.importorskip("reportlab")
    from app.utils.pdf_generator import InvoicePDFGenerator

    # Add an invoice item
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("75.00"),
    )
    db.session.add(item)

    # Add an extra good
    good = ExtraGood(
        name="Hardware Component",
        description="Raspberry Pi 4 Model B",
        category="product",
        quantity=Decimal("2.00"),
        unit_price=Decimal("55.00"),
        sku="RPI4-4GB",
        created_by=sample_user.id,
        invoice_id=sample_invoice.id,
    )
    db.session.add(good)
    db.session.commit()

    sample_invoice.calculate_totals()
    db.session.commit()

    # Generate PDF via main path (ReportLab template JSON)
    generator = InvoicePDFGenerator(sample_invoice)
    with app.test_request_context("/"):
        pdf_bytes = generator.generate_pdf()

    assert pdf_bytes is not None
    assert len(pdf_bytes) > 0
    assert pdf_bytes[:4] == b"%PDF"

    # PDF stores text in streams; item and extra good text should appear in raw bytes
    assert b"Development work" in pdf_bytes
    assert b"Hardware Component" in pdf_bytes
    assert b"Raspberry Pi 4 Model B" in pdf_bytes or b"RPI4-4GB" in pdf_bytes


@pytest.mark.smoke
@pytest.mark.invoices
def test_pdf_export_fallback_with_extra_goods_smoke(app, sample_invoice, sample_user):
    """Smoke test: Generate fallback PDF with extra goods without errors."""
    from app.utils.pdf_generator_fallback import InvoicePDFGeneratorFallback

    # Add items and goods
    item = InvoiceItemFactory(
        invoice_id=sample_invoice.id,
        description="Design Services",
        quantity=Decimal("20.00"),
        unit_price=Decimal("65.00"),
    )

    good = ExtraGood(
        name="Stock Photos",
        description="Premium stock photo bundle",
        category="material",
        quantity=Decimal("1.00"),
        unit_price=Decimal("199.00"),
        sku="STOCK-BUNDLE-PRO",
        created_by=sample_user.id,
        invoice_id=sample_invoice.id,
    )
    db.session.add(good)
    db.session.commit()

    # Calculate totals
    sample_invoice.calculate_totals()
    db.session.commit()

    # Generate PDF using fallback - should not raise any exceptions
    generator = InvoicePDFGeneratorFallback(sample_invoice)
    pdf_bytes = generator.generate_pdf()

    # Verify PDF was generated
    assert pdf_bytes is not None
    assert len(pdf_bytes) > 0
    assert pdf_bytes[:4] == b"%PDF"  # PDF magic number


# ===============================================
# Invoice Deletion Tests
# ===============================================


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_deletion_basic(app, sample_invoice):
    """Test that an invoice can be deleted."""
    invoice_id = sample_invoice.id
    invoice_number = sample_invoice.invoice_number

    # Verify invoice exists
    assert Invoice.query.get(invoice_id) is not None

    # Delete invoice
    db.session.delete(sample_invoice)
    db.session.commit()

    # Verify invoice is deleted
    assert Invoice.query.get(invoice_id) is None


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_deletion_cascades_to_items(app, sample_invoice):
    """Test that deleting an invoice also deletes its items (cascade)."""
    # Add items to invoice
    items = [
        InvoiceItem(
            invoice_id=sample_invoice.id,
            description="Development work",
            quantity=Decimal("10.00"),
            unit_price=Decimal("75.00"),
        ),
        InvoiceItem(
            invoice_id=sample_invoice.id,
            description="Design work",
            quantity=Decimal("5.00"),
            unit_price=Decimal("100.00"),
        ),
    ]

    for item in items:
        db.session.add(item)
    db.session.commit()

    # Store item IDs
    item_ids = [item.id for item in items]
    invoice_id = sample_invoice.id

    # Verify items exist
    for item_id in item_ids:
        assert InvoiceItem.query.get(item_id) is not None

    # Delete invoice
    db.session.delete(sample_invoice)
    db.session.commit()

    # Verify invoice is deleted
    assert Invoice.query.get(invoice_id) is None

    # Verify items are also deleted (cascade)
    for item_id in item_ids:
        assert InvoiceItem.query.get(item_id) is None


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_deletion_cascades_to_extra_goods(app, sample_invoice, sample_user):
    """Test that deleting an invoice also deletes its extra goods (cascade)."""
    # Add extra goods to invoice
    goods = [
        ExtraGood(
            name="Product A",
            category="product",
            quantity=Decimal("2.00"),
            unit_price=Decimal("50.00"),
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
        ExtraGood(
            name="Service B",
            category="service",
            quantity=Decimal("1.00"),
            unit_price=Decimal("100.00"),
            created_by=sample_user.id,
            invoice_id=sample_invoice.id,
        ),
    ]

    for good in goods:
        db.session.add(good)
    db.session.commit()

    # Store good IDs
    good_ids = [good.id for good in goods]
    invoice_id = sample_invoice.id

    # Verify goods exist
    for good_id in good_ids:
        assert ExtraGood.query.get(good_id) is not None

    # Delete invoice
    db.session.delete(sample_invoice)
    db.session.commit()

    # Verify invoice is deleted
    assert Invoice.query.get(invoice_id) is None

    # Verify goods are also deleted (cascade)
    for good_id in good_ids:
        assert ExtraGood.query.get(good_id) is None


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_deletion_cascades_to_payments(app, sample_invoice, sample_user):
    """Test that deleting an invoice also deletes its payments (cascade)."""
    from factories import PaymentFactory
    from app.models.payments import Payment

    # Add payments to invoice
    payments = [
        PaymentFactory(
            invoice_id=sample_invoice.id,
            amount=Decimal("100.00"),
            payment_date=date.today(),
            method="bank_transfer",
            received_by=sample_user.id,
        ),
        PaymentFactory(
            invoice_id=sample_invoice.id,
            amount=Decimal("200.00"),
            payment_date=date.today(),
            method="credit_card",
            received_by=sample_user.id,
        ),
    ]
    db.session.commit()

    # Store payment IDs
    payment_ids = [payment.id for payment in payments]
    invoice_id = sample_invoice.id

    # Verify payments exist
    for payment_id in payment_ids:
        assert Payment.query.get(payment_id) is not None

    # Delete invoice
    db.session.delete(sample_invoice)
    db.session.commit()

    # Verify invoice is deleted
    assert Invoice.query.get(invoice_id) is None

    # Verify payments are also deleted (cascade)
    for payment_id in payment_ids:
        assert Payment.query.get(payment_id) is None


@pytest.mark.unit
@pytest.mark.invoices
def test_invoice_deletion_with_all_related_data(app, sample_invoice, sample_user):
    """Test that deleting an invoice with all related data works correctly."""
    # Add items
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description="Development work",
        quantity=Decimal("10.00"),
        unit_price=Decimal("75.00"),
    )
    db.session.add(item)

    # Add extra goods
    good = ExtraGood(
        name="Product A",
        category="product",
        quantity=Decimal("1.00"),
        unit_price=Decimal("100.00"),
        created_by=sample_user.id,
        invoice_id=sample_invoice.id,
    )
    db.session.add(good)

    # Add payment
    from app.models.payments import Payment

    payment = Payment(
        invoice_id=sample_invoice.id,
        amount=Decimal("500.00"),
        payment_date=date.today(),
        method="bank_transfer",
        received_by=sample_user.id,
    )
    db.session.add(payment)
    db.session.commit()

    # Store IDs
    invoice_id = sample_invoice.id
    item_id = item.id
    good_id = good.id
    payment_id = payment.id

    # Verify all exist
    assert Invoice.query.get(invoice_id) is not None
    assert InvoiceItem.query.get(item_id) is not None
    assert ExtraGood.query.get(good_id) is not None
    assert Payment.query.get(payment_id) is not None

    # Delete invoice
    db.session.delete(sample_invoice)
    db.session.commit()

    # Verify all are deleted
    assert Invoice.query.get(invoice_id) is None
    assert InvoiceItem.query.get(item_id) is None
    assert ExtraGood.query.get(good_id) is None
    assert Payment.query.get(payment_id) is None


@pytest.mark.routes
@pytest.mark.invoices
def test_delete_invoice_route_success(app, client, user, project):
    """Test that the delete invoice route works correctly."""
    from app.models import Client

    # Authenticate
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    # Create client and invoice
    cl = ClientFactory(name="Delete Test Client", email="delete@test.com")
    db.session.commit()

    inv = InvoiceFactory(
        invoice_number="INV-DELETE-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=30),
        created_by=user.id,
        status="draft",
    )
    db.session.commit()

    invoice_id = inv.id

    # Delete invoice via route
    resp = client.post(f"/invoices/{invoice_id}/delete", follow_redirects=True)

    # Verify redirect to list page
    assert resp.status_code == 200

    # Verify invoice is deleted
    assert Invoice.query.get(invoice_id) is None

    # Verify success message in response
    html = resp.get_data(as_text=True)
    assert "deleted successfully" in html


@pytest.mark.routes
@pytest.mark.invoices
def test_delete_invoice_route_permission_denied(app, client, user, project):
    """Test that users cannot delete invoices they don't own."""
    from app.models import Client

    # Create another user
    other_user = UserFactory(username="otheruser", role="user")
    db.session.commit()

    # Create client and invoice owned by other_user
    cl = ClientFactory(name="Permission Test Client", email="perm@test.com")
    db.session.commit()

    inv = InvoiceFactory(
        invoice_number="INV-PERM-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=30),
        created_by=other_user.id,  # Owned by other_user
        status="draft",
    )
    db.session.commit()

    invoice_id = inv.id

    # Authenticate as regular user
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    # Try to delete invoice
    resp = client.post(f"/invoices/{invoice_id}/delete", follow_redirects=True)

    # Verify error message
    html = resp.get_data(as_text=True)
    assert "permission" in html.lower()

    # Verify invoice still exists
    assert Invoice.query.get(invoice_id) is not None


@pytest.mark.routes
@pytest.mark.invoices
def test_delete_invoice_route_admin_can_delete_any(app, client, user, project):
    """Test that admins can delete any invoice."""
    from app.models import Client

    # Create another user
    other_user = UserFactory(username="otheruseradmin", role="user")
    db.session.commit()

    # Create client and invoice owned by other_user
    cl = ClientFactory(name="Admin Delete Test Client", email="admin@test.com")
    db.session.commit()

    inv = InvoiceFactory(
        invoice_number="INV-ADMIN-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=30),
        created_by=other_user.id,  # Owned by other_user
        status="draft",
    )
    db.session.commit()

    invoice_id = inv.id

    # Make user an admin
    user.role = "admin"
    db.session.commit()

    # Authenticate as admin
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    # Delete invoice as admin
    resp = client.post(f"/invoices/{invoice_id}/delete", follow_redirects=True)

    # Verify success
    assert resp.status_code == 200

    # Verify invoice is deleted
    assert Invoice.query.get(invoice_id) is None


@pytest.mark.routes
@pytest.mark.invoices
def test_delete_invoice_route_not_found(app, client, user):
    """Test that deleting a non-existent invoice returns 404."""
    # Authenticate
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True

    # Try to delete non-existent invoice
    resp = client.post("/invoices/99999/delete")

    # Verify 404
    assert resp.status_code == 404


@pytest.mark.smoke
@pytest.mark.invoices
def test_invoice_view_has_delete_button(app, client, user, project):
    """Smoke test: Verify that the invoice view page has a delete button."""
    from app.models import Client

    # Authenticate using login endpoint
    client.post("/login", data={"username": user.username, "password": "password123"}, follow_redirects=True)

    # Create client and invoice
    cl = ClientFactory(name="Delete Button Test Client", email="button@test.com")
    db.session.commit()

    inv = InvoiceFactory(
        invoice_number="INV-BUTTON-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=30),
        created_by=user.id,
        status="draft",
    )
    db.session.commit()

    # Visit invoice view page
    resp = client.get(f"/invoices/{inv.id}")
    assert resp.status_code == 200

    html = resp.get_data(as_text=True)

    # Verify delete button exists
    assert "delete" in html.lower()
    # Check for modal elements
    assert "deleteInvoiceModal" in html
    assert f"showDeleteModal('{inv.id}'" in html or f'showDeleteModal("{inv.id}"' in html
    assert "Warning:" in html or "warning" in html.lower()
    # Verify the JavaScript function exists
    assert "function showDeleteModal" in html
    assert "deleteInvoiceForm" in html


@pytest.mark.routes
def test_invoice_view_peppol_check_exception_shows_generic_warning(app, client, user, project):
    """When PEPPOL compliance check raises, exception is caught and logged (no bare pass)."""
    from app.models import Client as ClientModel

    cl = ClientFactory(name="PEPPOL Test Client", email="peppol@test.com")
    db.session.commit()
    inv = InvoiceFactory(
        invoice_number="INV-PEPPOL-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=30),
        created_by=user.id,
        status="draft",
    )
    db.session.commit()
    with client.session_transaction() as sess:
        sess["_user_id"] = str(user.id)
        sess["_fresh"] = True
    original_get_custom_field = ClientModel.get_custom_field

    def raise_on_peppol(self, key, default=""):
        if key == "peppol_endpoint_id":
            raise AttributeError("test peppol config")
        return original_get_custom_field(self, key, default)

    with patch.object(Settings, "get_settings") as mock_settings:
        mock_settings.return_value = type("MockSettings", (), {"invoices_peppol_compliant": True})()
        with patch.object(ClientModel, "get_custom_field", raise_on_peppol):
            resp = client.get(f"/invoices/{inv.id}")
    # PEPPOL block must catch the exception (no unhandled AttributeError from raise_on_peppol)
    assert resp.status_code in (200, 500)
    # If we got 500, it must not be from our PEPPOL exception (traceback would mention test file)
    if resp.status_code == 500:
        body = resp.get_data(as_text=True)
        assert "raise_on_peppol" not in body and "test peppol config" not in body


@pytest.mark.smoke
@pytest.mark.invoices
@pytest.mark.skip(reason="Temporarily disabled due to intermittent ObjectDeletedError in CI")
def test_invoice_list_has_delete_buttons(app, client, admin_user, project):
    """Smoke test: Verify that the invoice list page has delete buttons."""
    from app.models import Client

    # Capture project_id early to avoid any session expiration across requests
    project_id = project.id

    # Authenticate as admin using login endpoint
    client.post("/login", data={"username": admin_user.username, "password": "password123"}, follow_redirects=True)

    # Create client and invoices
    cl = Client(name="List Delete Test Client", email="listdelete@test.com")
    db.session.add(cl)
    db.session.commit()

    # Ensure project still exists post-login (reattach or recreate if needed)
    from app.models import Project as ProjectModel

    proj = ProjectModel.query.get(project_id)
    if proj is None:
        # Recreate a minimal billable project tied to the client for stability
        proj = ProjectModel(name="Smoke Test Project", client_id=cl.id, billable=True, hourly_rate=Decimal("75.00"))
        db.session.add(proj)
        db.session.commit()
        project_id = proj.id

    invoices = [
        Invoice(
            invoice_number=f"INV-LIST-{i:03d}",
            project_id=project_id,
            client_name=cl.name,
            client_id=cl.id,
            due_date=date.today() + timedelta(days=30),
            created_by=admin_user.id,
        )
        for i in range(1, 4)
    ]

    for inv in invoices:
        db.session.add(inv)
    db.session.commit()

    # Visit invoice list page
    resp = client.get("/invoices")
    assert resp.status_code == 200

    html = resp.get_data(as_text=True)

    # Verify delete buttons exist for each invoice
    for inv in invoices:
        assert f"showDeleteModal({inv.id}" in html

    # Verify modal exists
    assert "deleteInvoiceModal" in html
    assert "showDeleteModal" in html


@pytest.mark.smoke
@pytest.mark.invoices
def test_delete_invoice_with_complex_data_smoke(app, client, user, project):
    """Smoke test: Delete an invoice with items, goods, and payments."""
    from app.models import Client
    from app.models.payments import Payment

    # Authenticate using login endpoint
    client.post("/login", data={"username": user.username, "password": "password123"}, follow_redirects=True)

    # Create client and invoice
    cl = Client(name="Complex Delete Test", email="complex@test.com")
    db.session.add(cl)
    db.session.commit()

    inv = Invoice(
        invoice_number="INV-COMPLEX-001",
        project_id=project.id,
        client_name=cl.name,
        client_id=cl.id,
        due_date=date.today() + timedelta(days=30),
        created_by=user.id,
    )
    db.session.add(inv)
    db.session.commit()

    # Add items
    items = [
        InvoiceItem(invoice_id=inv.id, description=f"Item {i}", quantity=Decimal("5.00"), unit_price=Decimal("50.00"))
        for i in range(1, 4)
    ]
    for item in items:
        db.session.add(item)

    # Add extra goods
    goods = [
        ExtraGood(
            name=f"Good {i}",
            category="product",
            quantity=Decimal("1.00"),
            unit_price=Decimal("100.00"),
            created_by=user.id,
            invoice_id=inv.id,
        )
        for i in range(1, 3)
    ]
    for good in goods:
        db.session.add(good)

    # Add payments
    from app.models.payments import Payment

    payments = [
        PaymentFactory(
            invoice_id=inv.id,
            amount=Decimal("100.00"),
            payment_date=date.today(),
            method="bank_transfer",
            received_by=user.id,
        ),
        PaymentFactory(
            invoice_id=inv.id,
            amount=Decimal("200.00"),
            payment_date=date.today(),
            method="credit_card",
            received_by=user.id,
        ),
    ]

    db.session.commit()

    invoice_id = inv.id

    # Delete invoice
    resp = client.post(f"/invoices/{invoice_id}/delete", follow_redirects=True)

    # Verify success
    assert resp.status_code == 200
    html = resp.get_data(as_text=True)
    assert "deleted successfully" in html.lower()

    # Verify invoice and all related data are deleted
    assert Invoice.query.get(invoice_id) is None
