import pytest
from datetime import datetime, date, timedelta
from decimal import Decimal
from app import create_app, db
from app.models import User, Project, Invoice, InvoiceItem, Settings

@pytest.fixture
def app():
    """Create and configure a new app instance for each test."""
    app = create_app()
    app.config['TESTING'] = True
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture
def client(app):
    """A test client for the app."""
    return app.test_client()

@pytest.fixture
def runner(app):
    """A test runner for the app's Click commands."""
    return app.test_cli_runner()

@pytest.fixture
def sample_user(app):
    """Create a sample user for testing."""
    user = User(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."""
    project = Project(
        name='Test Project',
        client='Test Client',
        description='A test project',
        billable=True,
        hourly_rate=Decimal('75.00')
    )
    db.session.add(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 = Client(
        name='Test Client',
        email='client@test.com',
        created_by=sample_user.id
    )
    db.session.add(client)
    db.session.commit()
    
    invoice = Invoice(
        invoice_number='INV-20241201-001',
        project_id=sample_project.id,
        client_name='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()
    return invoice

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='Test Client',
        email='client@test.com',
        created_by=sample_user.id
    )
    db.session.add(client)
    db.session.commit()
    
    invoice = Invoice(
        invoice_number='INV-20241201-002',
        project_id=sample_project.id,
        client_name='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 == 'Test Client'
    assert invoice.status == 'draft'
    assert invoice.tax_rate == Decimal('20.00')

def test_invoice_item_creation(app, sample_invoice):
    """Test that invoice items can be created correctly."""
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description='Development work',
        quantity=Decimal('10.00'),
        unit_price=Decimal('75.00')
    )
    
    db.session.add(item)
    db.session.commit()
    
    assert item.id is not None
    assert item.total_amount == Decimal('750.00')
    assert item.invoice_id == sample_invoice.id

def test_invoice_totals_calculation(app, sample_invoice):
    """Test that invoice totals are calculated correctly."""
    # Add multiple items
    item1 = InvoiceItem(
        invoice_id=sample_invoice.id,
        description='Development work',
        quantity=Decimal('10.00'),
        unit_price=Decimal('75.00')
    )
    
    item2 = InvoiceItem(
        invoice_id=sample_invoice.id,
        description='Design work',
        quantity=Decimal('5.00'),
        unit_price=Decimal('100.00')
    )
    
    db.session.add_all([item1, item2])
    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 = Client(
        name='Test Client',
        email='client@test.com',
        created_by=sample_user.id
    )
    db.session.add(client)
    db.session.commit()
    
    invoice = Invoice(
        invoice_number='INV-20241201-003',
        project_id=sample_project.id,
        client_name='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()
    
    # Add item
    item = InvoiceItem(
        invoice_id=invoice.id,
        description='Development work',
        quantity=Decimal('10.00'),
        unit_price=Decimal('75.00')
    )
    
    db.session.add(item)
    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
    
    # Mock the current date to ensure consistent testing
    from unittest.mock import patch
    from datetime import datetime
    
    with patch('app.models.invoice.datetime') as mock_datetime:
        mock_datetime.utcnow.return_value = datetime(2024, 12, 1, 12, 0, 0)
        
        # First invoice of the day
        invoice_number = Invoice.generate_invoice_number()
        assert invoice_number == 'INV-20241201-001'
        
        # Create an invoice with this number
        project = Project(name='Test', client='Test Client', billable=True)
        user = User(username='testuser', role='user')
        db.session.add_all([project, user])
        db.session.commit()
        
        invoice = Invoice(
            invoice_number=invoice_number,
            project_id=project.id,
            client_name='Test Client',
            due_date=date.today() + timedelta(days=30),
            created_by=user.id
        )
        db.session.add(invoice)
        db.session.commit()
        
        # Next invoice should be numbered 002
        next_invoice_number = Invoice.generate_invoice_number()
        assert next_invoice_number == 'INV-20241201-002'

def test_invoice_overdue_status(app, sample_user, sample_project):
    """Test that invoices are marked as overdue correctly."""
    # Create an overdue invoice
    overdue_date = date.today() - timedelta(days=5)
    invoice = Invoice(
        invoice_number='INV-20241201-004',
        project_id=sample_project.id,
        client_name='Test Client',
        due_date=overdue_date,
        created_by=sample_user.id,
        status='sent'
    )
    
    db.session.add(invoice)
    db.session.commit()
    
    assert invoice.is_overdue == True
    assert invoice.days_overdue == 5
    
    # Test that status updates to overdue
    invoice.calculate_totals()
    assert invoice.status == 'overdue'

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 = InvoiceItem(
        invoice_id=sample_invoice.id,
        description='Test item',
        quantity=Decimal('5.00'),
        unit_price=Decimal('50.00')
    )
    
    db.session.add(item)
    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

# 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='Test Client',
        email='client@test.com',
        created_by=sample_user.id
    )
    db.session.add(client)
    db.session.commit()
    
    invoice = Invoice(
        invoice_number='INV-20241201-005',
        project_id=sample_project.id,
        client_name='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."""
    # Set up invoice with items
    item = InvoiceItem(
        invoice_id=sample_invoice.id,
        description='Development work',
        quantity=Decimal('10.00'),
        unit_price=Decimal('75.00')
    )
    db.session.add(item)
    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."""
    # 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."""
    # 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."""
    # 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
    
    # 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."""
    # 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