#![no_main]
//! Fuzz target for the booking engine.
//!
//! Generates structured transaction inputs with varying cost specs,
//! booking methods, and inventory states to find panics, overflows,
//! or other crashes in the booking and interpolation logic.

use arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use rust_decimal::Decimal;
use rustledger_booking::BookingEngine;
use rustledger_core::{
    Amount, BookingMethod, CostSpec, IncompleteAmount, NaiveDate, Posting, Transaction,
};

/// Fuzzer-friendly booking method selector.
#[derive(Debug, Arbitrary)]
enum FuzzBookingMethod {
    Strict,
    StrictWithSize,
    Fifo,
    Lifo,
    Hifo,
    Average,
    None,
}

impl From<FuzzBookingMethod> for BookingMethod {
    fn from(m: FuzzBookingMethod) -> Self {
        match m {
            FuzzBookingMethod::Strict => BookingMethod::Strict,
            FuzzBookingMethod::StrictWithSize => BookingMethod::StrictWithSize,
            FuzzBookingMethod::Fifo => BookingMethod::Fifo,
            FuzzBookingMethod::Lifo => BookingMethod::Lifo,
            FuzzBookingMethod::Hifo => BookingMethod::Hifo,
            FuzzBookingMethod::Average => BookingMethod::Average,
            FuzzBookingMethod::None => BookingMethod::None,
        }
    }
}

/// Fuzzer-friendly cost spec configuration.
#[derive(Debug, Arbitrary)]
struct FuzzCostSpec {
    /// Per-unit cost (cents, to avoid huge decimals)
    number_per: Option<i32>,
    /// Total cost
    number_total: Option<i32>,
    /// Whether to use a cost currency
    has_currency: bool,
    /// Whether to merge lots (average cost)
    merge: bool,
}

/// Fuzzer-friendly posting configuration.
#[derive(Debug, Arbitrary)]
struct FuzzPosting {
    /// Account index (0-4, maps to predefined accounts)
    account_idx: u8,
    /// Amount in cents (to keep decimals reasonable)
    amount_cents: i32,
    /// Currency index (0=USD, 1=EUR, 2=CORP)
    currency_idx: u8,
    /// Optional cost spec
    cost: Option<FuzzCostSpec>,
    /// Whether this posting has a missing amount (for interpolation)
    missing_amount: bool,
}

/// Fuzzer-friendly transaction with multiple postings.
#[derive(Debug, Arbitrary)]
struct FuzzTransaction {
    /// Booking method to use
    booking_method: FuzzBookingMethod,
    /// Year offset (2020-2025)
    year_offset: u8,
    /// Month (1-12)
    month: u8,
    /// Day (1-28)
    day: u8,
    /// Postings (2-8, filtered at runtime)
    postings: Vec<FuzzPosting>,
    /// Optional prior transactions to build inventory state
    prior_buys: Vec<FuzzPriorBuy>,
}

/// A prior buy transaction to populate inventory before the main transaction.
#[derive(Debug, Arbitrary)]
struct FuzzPriorBuy {
    /// Number of units to buy (whole units, not cents)
    units: u16,
    /// Cost per unit in cents
    cost_cents: u16,
    year_offset: u8,
}

const ACCOUNTS: &[&str] = &[
    "Assets:Stock",
    "Assets:Cash",
    "Expenses:Fees",
    "Income:Gains",
    "Equity:Opening",
];

const CURRENCIES: &[&str] = &["USD", "EUR", "CORP"];

fn make_date(year_offset: u8, month: u8, day: u8) -> NaiveDate {
    let year = 2020 + (year_offset % 6) as i32;
    let month = ((month % 12) + 1) as u32;
    let day = ((day % 28) + 1) as u32;
    rustledger_core::naive_date(year, month, day)
        .unwrap_or(rustledger_core::naive_date(2020, 1, 1).unwrap())
}

fn make_decimal(cents: i32) -> Decimal {
    Decimal::new(cents as i64, 2)
}

fuzz_target!(|input: FuzzTransaction| {
    // Need at least 2 postings for a meaningful transaction
    if input.postings.len() < 2 || input.postings.len() > 8 {
        return;
    }

    let method: BookingMethod = input.booking_method.into();
    let mut engine = BookingEngine::with_method(method);

    let date = make_date(input.year_offset, input.month, input.day);

    // Build prior inventory state with buy transactions
    for (i, buy) in input.prior_buys.iter().take(5).enumerate() {
        if buy.units == 0 || buy.cost_cents == 0 {
            continue;
        }
        let buy_date = make_date(buy.year_offset, 1, 1);
        let units = Decimal::new(buy.units as i64, 0);
        let cost = make_decimal(buy.cost_cents as i32);

        let posting = Posting::new("Assets:Stock", Amount::new(units, "CORP"))
            .with_cost(CostSpec::empty().with_number_per(cost).with_currency("USD"));
        let counter = Posting::new("Assets:Cash", Amount::new(-units * cost, "USD"));

        let txn = Transaction::new(buy_date, format!("Buy {i}"))
            .with_posting(posting)
            .with_posting(counter);

        // Ignore errors — we're building up state, some combos may fail
        if let Ok(result) = engine.book_and_interpolate(&txn) {
            engine.apply(&result.transaction);
        }
    }

    // Build the main transaction
    let mut txn = Transaction::new(date, "Fuzz transaction");

    for fuzz_posting in &input.postings {
        let account = ACCOUNTS[(fuzz_posting.account_idx as usize) % ACCOUNTS.len()];
        let currency = CURRENCIES[(fuzz_posting.currency_idx as usize) % CURRENCIES.len()];

        if fuzz_posting.missing_amount {
            // Posting with missing amount (for interpolation)
            let posting =
                Posting::with_incomplete(account, IncompleteAmount::CurrencyOnly(currency.into()));
            txn = txn.with_posting(posting);
        } else {
            let amount = Amount::new(make_decimal(fuzz_posting.amount_cents), currency);
            let mut posting = Posting::new(account, amount);

            if let Some(ref cost) = fuzz_posting.cost {
                let mut spec = CostSpec::empty();
                if let Some(per) = cost.number_per {
                    spec = spec.with_number_per(make_decimal(per));
                }
                if let Some(total) = cost.number_total {
                    spec = spec.with_number_total(make_decimal(total));
                }
                if cost.has_currency {
                    spec = spec.with_currency("USD");
                }
                if cost.merge {
                    spec = spec.with_merge();
                }
                posting = posting.with_cost(spec);
            }

            txn = txn.with_posting(posting);
        }
    }

    // The booking engine must never panic, regardless of input.
    // Errors are expected and fine — panics are bugs.
    let _ = engine.book_and_interpolate(&txn);
});
