const std = @import("std");
const Date = @import("../Date.zig");
const Candle = @import("../models/candle.zig").Candle;
const Dividend = @import("../models/dividend.zig").Dividend;
const Split = @import("../models/split.zig").Split;
const portfolio = @import("../models/portfolio.zig");

/// Minimum holding period (in years) before annualizing returns.
/// Set below 1.0 to handle trading-day snap (e.g. a "1-year" lookback
/// that lands on 362 days due to weekends).
const min_annualize_years = 0.95;

/// Compute CAGR from a total return over a given number of years.
/// Returns null for periods shorter than `min_annualize_years` where
/// extrapolating to a full year would be misleading.
inline fn annualizedReturn(total: f64, years: f64) ?f64 {
    if (years < min_annualize_years) return null;
    return std.math.pow(f64, 1.0 + total, 1.0 / years) - 1.0;
}

/// Performance calculation results, Morningstar-style.
pub const PerformanceResult = struct {
    /// Total return over the period (e.g., 0.25 = 25%)
    total_return: f64,
    /// Annualized return (for periods > 1 year)
    annualized_return: ?f64,
    /// Start date used
    from: Date,
    /// End date used
    to: Date,
};

/// Compute total return from adjusted close prices.
/// Candles must be sorted by date ascending.
/// `from` snaps backward (last trading day on/before), `to` snaps backward.
pub fn totalReturnFromAdjClose(candles: []const Candle, from: Date, to: Date) ?PerformanceResult {
    return totalReturnFromAdjCloseSnap(candles, from, to, .backward);
}

/// Same as totalReturnFromAdjClose but both dates snap backward
/// (last trading day on or before). Used for month-end methodology where
/// both from and to represent month-end reference dates.
fn totalReturnFromAdjCloseBackward(candles: []const Candle, from: Date, to: Date) ?PerformanceResult {
    return totalReturnFromAdjCloseSnap(candles, from, to, .backward);
}

fn totalReturnFromAdjCloseSnap(candles: []const Candle, from: Date, to: Date, start_dir: SearchDirection) ?PerformanceResult {
    const start = findNearestCandle(candles, from, start_dir) orelse return null;
    const end = findNearestCandle(candles, to, .backward) orelse return null;

    if (start.adj_close == 0) return null;

    const total = (end.adj_close / start.adj_close) - 1.0;
    const years = Date.yearsBetween(start.date, end.date);

    return .{
        .total_return = total,
        .annualized_return = annualizedReturn(total, years),
        .from = start.date,
        .to = end.date,
    };
}

/// Compute total return with manual dividend reinvestment.
/// Uses raw close prices and dividend records independently.
/// Candles and dividends must be sorted by date ascending.
/// `from` snaps backward (last trading day on/before), `to` snaps backward.
pub fn totalReturnWithDividends(
    candles: []const Candle,
    dividends: []const Dividend,
    from: Date,
    to: Date,
) ?PerformanceResult {
    return totalReturnWithDividendsSnap(candles, dividends, from, to, .backward);
}

/// Same as totalReturnWithDividends but both dates snap backward.
fn totalReturnWithDividendsBackward(
    candles: []const Candle,
    dividends: []const Dividend,
    from: Date,
    to: Date,
) ?PerformanceResult {
    return totalReturnWithDividendsSnap(candles, dividends, from, to, .backward);
}

fn totalReturnWithDividendsSnap(
    candles: []const Candle,
    dividends: []const Dividend,
    from: Date,
    to: Date,
    start_dir: SearchDirection,
) ?PerformanceResult {
    // When `from` predates all cached candles, the only safe shortcut is
    // for stable-NAV funds: if the earliest candle we have is at $1, we
    // can extrapolate backward and synthesize a $1 candle at `from`.
    // Reuse `stableNavCandle` so the synthesized candle has the same
    // shape as the one used elsewhere.
    const start = findNearestCandle(candles, from, start_dir) orelse
        if (candles.len > 0 and from.lessThan(candles[0].date) and candles[0].close == 1.0)
            portfolio.stableNavCandle(from)
        else
            return null;
    const end = findNearestCandle(candles, to, .backward) orelse return null;

    if (start.close == 0) return null;

    // Simulate: start with 1 share, reinvest dividends at ex-date close
    var shares: f64 = 1.0;

    for (dividends) |div| {
        if (div.ex_date.lessThan(start.date)) continue;
        if (end.date.lessThan(div.ex_date)) continue;

        // Find close price on or near the ex-date.
        // For stable-NAV funds, dividends before candle history use $1.
        const price_candle = findNearestCandle(candles, div.ex_date, .backward) orelse
            if (start.close == 1.0)
                portfolio.stableNavCandle(div.ex_date)
            else
                continue;
        if (price_candle.close > 0) {
            shares += (div.amount * shares) / price_candle.close;
        }
    }

    const final_value = shares * end.close;
    const total = (final_value / start.close) - 1.0;
    const years = Date.yearsBetween(start.date, end.date);

    return .{
        .total_return = total,
        .annualized_return = annualizedReturn(total, years),
        .from = start.date,
        .to = end.date,
    };
}

// Stable-NAV candle synthesis lives in src/models/portfolio.zig
// (`portfolio.stableNavCandle`) so every caller agrees on the shape
// of a synthesized $1 candle. Performance-only heuristics stay here.

/// Convenience: compute 1yr, 3yr, 5yr, 10yr trailing returns from adjusted close.
/// Uses the last available date as the endpoint.
pub const TrailingReturns = struct {
    one_year: ?PerformanceResult = null,
    three_year: ?PerformanceResult = null,
    five_year: ?PerformanceResult = null,
    ten_year: ?PerformanceResult = null,
    /// 1-week return (non-annualized; just the latest close vs the
    /// close ~7 days back). `?f64` rather than `?PerformanceResult`
    /// because there's no meaningful annualization over a 1-week
    /// window and `from`/`to` don't add information for a fixed
    /// 7-day shift. Matches `benchmark.ReturnsByPeriod.week`'s shape.
    week: ?f64 = null,
};

/// Merge adj_close and dividend-reinvestment returns, preferring the higher
/// annualized return for each period. This works because:
///   - When dividend data is complete: reinvestment >= adj_close (compounding)
///   - When dividend data is incomplete: adj_close > reinvestment (missing dividends)
/// So the higher value is always the more correct one.
pub fn withDividendFallback(div_returns: TrailingReturns, adj_close_returns: TrailingReturns) TrailingReturns {
    return .{
        .one_year = bestResult(div_returns.one_year, adj_close_returns.one_year),
        .three_year = bestResult(div_returns.three_year, adj_close_returns.three_year),
        .five_year = bestResult(div_returns.five_year, adj_close_returns.five_year),
        .ten_year = bestResult(div_returns.ten_year, adj_close_returns.ten_year),
        // Week returns aren't annualized and the dividend-fallback
        // semantic doesn't apply (a 1-week window is too short for
        // dividend compounding to matter). Just prefer the dividend
        // side when present, fall through to adj_close otherwise.
        .week = div_returns.week orelse adj_close_returns.week,
    };
}

fn bestResult(a: ?PerformanceResult, b: ?PerformanceResult) ?PerformanceResult {
    const aa = a orelse return b;
    const bb = b orelse return a;
    const a_ann = aa.annualized_return orelse return b;
    const b_ann = bb.annualized_return orelse return a;
    return if (a_ann >= b_ann) a else b;
}

/// Trailing returns from exact calendar date N years ago to latest candle date.
/// Start dates snap forward to the next trading day (e.g., weekend → Monday).
pub fn trailingReturns(candles: []const Candle) TrailingReturns {
    if (candles.len == 0) return .{};

    const end_date = candles[candles.len - 1].date;

    return .{
        .one_year = totalReturnFromAdjClose(candles, end_date.subtractYears(1), end_date),
        .three_year = totalReturnFromAdjClose(candles, end_date.subtractYears(3), end_date),
        .five_year = totalReturnFromAdjClose(candles, end_date.subtractYears(5), end_date),
        .ten_year = totalReturnFromAdjClose(candles, end_date.subtractYears(10), end_date),
        .week = weekReturn(candles),
    };
}

/// Same as trailingReturns but with dividend reinvestment.
pub fn trailingReturnsWithDividends(
    candles: []const Candle,
    dividends: []const Dividend,
) TrailingReturns {
    if (candles.len == 0) return .{};

    const end_date = candles[candles.len - 1].date;

    return .{
        .one_year = totalReturnWithDividends(candles, dividends, end_date.subtractYears(1), end_date),
        .three_year = totalReturnWithDividends(candles, dividends, end_date.subtractYears(3), end_date),
        .five_year = totalReturnWithDividends(candles, dividends, end_date.subtractYears(5), end_date),
        .ten_year = totalReturnWithDividends(candles, dividends, end_date.subtractYears(10), end_date),
        .week = weekReturn(candles),
    };
}

/// Morningstar-style trailing returns using month-end reference dates.
/// End date = last calendar day of prior month. Start date = that month-end minus N years.
/// Both dates snap backward to the last trading day on or before, matching
/// Morningstar's "last business day of the month" convention.
pub fn trailingReturnsMonthEnd(candles: []const Candle, as_of: Date) TrailingReturns {
    if (candles.len == 0) return .{};

    // End reference = last day of the prior month (snaps backward to last trading day)
    const month_end = as_of.lastDayOfPriorMonth();

    return .{
        .one_year = totalReturnFromAdjCloseBackward(candles, month_end.subtractYears(1), month_end),
        .three_year = totalReturnFromAdjCloseBackward(candles, month_end.subtractYears(3), month_end),
        .five_year = totalReturnFromAdjCloseBackward(candles, month_end.subtractYears(5), month_end),
        .ten_year = totalReturnFromAdjCloseBackward(candles, month_end.subtractYears(10), month_end),
    };
}

/// Same as trailingReturnsMonthEnd but with dividend reinvestment.
pub fn trailingReturnsMonthEndWithDividends(
    candles: []const Candle,
    dividends: []const Dividend,
    as_of: Date,
) TrailingReturns {
    if (candles.len == 0) return .{};

    const month_end = as_of.lastDayOfPriorMonth();

    return .{
        .one_year = totalReturnWithDividendsBackward(candles, dividends, month_end.subtractYears(1), month_end),
        .three_year = totalReturnWithDividendsBackward(candles, dividends, month_end.subtractYears(3), month_end),
        .five_year = totalReturnWithDividendsBackward(candles, dividends, month_end.subtractYears(5), month_end),
        .ten_year = totalReturnWithDividendsBackward(candles, dividends, month_end.subtractYears(10), month_end),
    };
}

// ── Price-only returns (split-adjusted, NOT dividend-adjusted) ──────
//
// Why this exists: most providers' `adj_close` field is split-AND-
// dividend adjusted (Tiingo) or split-only (Polygon). Using
// `adj_close` ratios produces "total return" for Tiingo data (which
// is what the rest of this module computes). For an apples-to-apples
// comparison against the price-return numbers most public sources
// publish (Yahoo chart bar, FMP, Barchart, Fidelity stock pages), we
// need a series adjusted for splits but NOT dividends.
//
// We synthesize that here by walking the splits list and applying
// ratios to raw `close` directly. NKE has no splits in our windows,
// NVDA has a 10:1 in 2024-06-10 — both correctly handled.

/// Compute split-adjusted-but-not-dividend-adjusted return.
///
/// Both dates use `start_dir`/`backward` snap.
///
/// **Provider semantics:** Tiingo and Polygon both deliver `close`
/// values that are the **unadjusted historical market prices** —
/// pre-split candles' close fields show the actual market price on
/// that day, NOT divided by the cumulative split ratio. Verified for
/// AAPL (2016-04-04 close $111.12, the actual market price; 4:1
/// split in 2020 reflected as a real price drop on that day) and
/// NVDA (2021-07-19 close $751.19, actual pre-split price; 2024-06-10
/// 10:1 split reflected as real price drop).
///
/// To compute price-only return that's apples-to-apples between
/// pre- and post-split candles, we apply each split in the window
/// to the start price. Example: NVDA 5Y window ending 2026-05-19
/// starts ~2021-05-19 with close ~$750 (pre-2021-split). The 2021
/// 4:1 and 2024 10:1 splits both fall in the window, so cumulative
/// ratio = 40. adj_start = $750 / 40 = $18.75. End close ~$220.
/// Return = 220/18.75 - 1 ≈ +1073% over 5y ≈ +63% ann (which is
/// what NVDA's actual 5Y price return is per Morningstar/Koyfin).
fn priceReturnSnap(
    candles: []const Candle,
    splits: []const Split,
    from: Date,
    to: Date,
    start_dir: SearchDirection,
) ?PerformanceResult {
    const start = findNearestCandle(candles, from, start_dir) orelse return null;
    const end = findNearestCandle(candles, to, .backward) orelse return null;

    if (start.close == 0) return null;

    // Cumulative split adjustment: for each split between start.date
    // (exclusive) and end.date (inclusive), the start price needs to
    // be divided by the cumulative ratio to make it post-split-
    // equivalent with the end price.
    var cum_ratio: f64 = 1.0;
    for (splits) |s| {
        if (s.date.lessThan(start.date) or s.date.eql(start.date)) continue;
        if (end.date.lessThan(s.date)) continue;
        cum_ratio *= s.ratio();
    }

    const adj_start_close = start.close / cum_ratio;
    if (adj_start_close == 0) return null;

    const total = (end.close / adj_start_close) - 1.0;
    const years = Date.yearsBetween(start.date, end.date);

    return .{
        .total_return = total,
        .annualized_return = annualizedReturn(total, years),
        .from = start.date,
        .to = end.date,
    };
}

/// Trailing price-only returns from exact calendar date N years ago to
/// latest candle date. Start dates snap forward (matching the
/// `trailingReturns` behavior). Splits in the window are applied to
/// the start close so the comparison is split-adjusted.
pub fn trailingReturnsPriceOnly(candles: []const Candle, splits: []const Split) TrailingReturns {
    if (candles.len == 0) return .{};

    const end_date = candles[candles.len - 1].date;

    return .{
        .one_year = priceReturnSnap(candles, splits, end_date.subtractYears(1), end_date, .forward),
        .three_year = priceReturnSnap(candles, splits, end_date.subtractYears(3), end_date, .forward),
        .five_year = priceReturnSnap(candles, splits, end_date.subtractYears(5), end_date, .forward),
        .ten_year = priceReturnSnap(candles, splits, end_date.subtractYears(10), end_date, .forward),
        .week = weekReturn(candles),
    };
}

/// Month-end trailing price-only returns (split-adjusted only).
pub fn trailingReturnsPriceOnlyMonthEnd(
    candles: []const Candle,
    splits: []const Split,
    as_of: Date,
) TrailingReturns {
    if (candles.len == 0) return .{};

    const month_end = as_of.lastDayOfPriorMonth();

    return .{
        .one_year = priceReturnSnap(candles, splits, month_end.subtractYears(1), month_end, .backward),
        .three_year = priceReturnSnap(candles, splits, month_end.subtractYears(3), month_end, .backward),
        .five_year = priceReturnSnap(candles, splits, month_end.subtractYears(5), month_end, .backward),
        .ten_year = priceReturnSnap(candles, splits, month_end.subtractYears(10), month_end, .backward),
    };
}

const SearchDirection = enum { forward, backward };

/// Maximum calendar days a snapped candle can be from the target date.
/// Covers weekends + holidays (e.g., Christmas week). Beyond this, the data
/// is likely missing and the result would be misleading.
const max_snap_days: i32 = 10;

fn findNearestCandle(candles: []const Candle, target: Date, direction: SearchDirection) ?Candle {
    if (candles.len == 0) return null;

    // Binary search: lo = first index where candles[lo].date >= target
    var lo: usize = 0;
    var hi: usize = candles.len;
    while (lo < hi) {
        const mid = lo + (hi - lo) / 2;
        if (candles[mid].date.lessThan(target)) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }

    const candidate = switch (direction) {
        // First candle on or after target
        .forward => if (lo < candles.len) candles[lo] else return null,
        // Last candle on or before target; if target is before all data,
        // fall back to first candle (snap distance check will reject if too far)
        .backward => if (lo < candles.len and candles[lo].date.eql(target))
            candles[lo]
        else if (lo > 0)
            candles[lo - 1]
        else
            candles[0],
    };

    // Reject if the snap distance exceeds tolerance
    const gap = candidate.date.days - target.days;
    if (gap > max_snap_days or gap < -max_snap_days) return null;

    return candidate;
}

/// Format a return value as a percentage string (e.g., "12.34%")
pub fn formatReturn(buf: []u8, value: f64) []const u8 {
    return std.fmt.bufPrint(buf, "{d:.2}%", .{value * 100.0}) catch "??%";
}

/// Compute 1-week return from candle data: (latest_close / close_7_days_ago) - 1.
/// Candles must be sorted by date ascending.
pub fn weekReturn(candles: []const Candle) ?f64 {
    if (candles.len < 2) return null;
    const latest = candles[candles.len - 1];
    const target_date = latest.date.addDays(-7);

    // Linear scan backward (at most ~10 steps for daily candles)
    var i: usize = candles.len - 2;
    while (true) {
        if (candles[i].date.days <= target_date.days) {
            if (candles[i].close == 0) return null;
            return (latest.close / candles[i].close) - 1.0;
        }
        if (i == 0) break;
        i -= 1;
    }
    return null;
}

test "weekReturn less than 2 candles" {
    const c = [_]Candle{.{ .date = Date.fromYmd(2024, 1, 10), .open = 100, .high = 100, .low = 100, .close = 100, .adj_close = 100, .volume = 0 }};
    try std.testing.expect(weekReturn(&c) == null);
    try std.testing.expect(weekReturn(&[_]Candle{}) == null);
}

test "weekReturn simple positive" {
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 1), .open = 100, .high = 100, .low = 100, .close = 100, .adj_close = 100, .volume = 0 },
        .{ .date = Date.fromYmd(2024, 1, 8), .open = 110, .high = 110, .low = 110, .close = 110, .adj_close = 110, .volume = 0 },
    };
    const r = weekReturn(&candles).?;
    try std.testing.expectApproxEqAbs(@as(f64, 0.10), r, 0.001);
}

test "weekReturn negative" {
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 1), .open = 100, .high = 100, .low = 100, .close = 100, .adj_close = 100, .volume = 0 },
        .{ .date = Date.fromYmd(2024, 1, 8), .open = 95, .high = 95, .low = 95, .close = 95, .adj_close = 95, .volume = 0 },
    };
    const r = weekReturn(&candles).?;
    try std.testing.expectApproxEqAbs(@as(f64, -0.05), r, 0.001);
}

test "weekReturn snaps to nearest trading day" {
    // Latest is Wednesday Jan 10. 7 days back is Wednesday Jan 3.
    // But candles only have Mon Jan 1 and Fri Jan 5. Should snap to Fri Jan 5.
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 1), .open = 90, .high = 90, .low = 90, .close = 90, .adj_close = 90, .volume = 0 },
        .{ .date = Date.fromYmd(2024, 1, 5), .open = 100, .high = 100, .low = 100, .close = 100, .adj_close = 100, .volume = 0 },
        .{ .date = Date.fromYmd(2024, 1, 10), .open = 105, .high = 105, .low = 105, .close = 105, .adj_close = 105, .volume = 0 },
    };
    // Target: Jan 10 - 7 = Jan 3. Nearest at-or-before is Jan 1 (not Jan 5 which is after Jan 3)
    const r = weekReturn(&candles).?;
    // 105/90 - 1 = 0.1667
    try std.testing.expectApproxEqAbs(@as(f64, 0.1667), r, 0.001);
}

test "weekReturn zero close returns null" {
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 1), .open = 0, .high = 0, .low = 0, .close = 0, .adj_close = 0, .volume = 0 },
        .{ .date = Date.fromYmd(2024, 1, 8), .open = 100, .high = 100, .low = 100, .close = 100, .adj_close = 100, .volume = 0 },
    };
    try std.testing.expect(weekReturn(&candles) == null);
}

test "total return simple" {
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 2), .open = 100, .high = 101, .low = 99, .close = 100, .adj_close = 100, .volume = 1000 },
        .{ .date = Date.fromYmd(2024, 6, 28), .open = 110, .high = 111, .low = 109, .close = 110, .adj_close = 110, .volume = 1000 },
        .{ .date = Date.fromYmd(2024, 12, 31), .open = 120, .high = 121, .low = 119, .close = 120, .adj_close = 120, .volume = 1000 },
    };
    const result = totalReturnFromAdjClose(&candles, Date.fromYmd(2024, 1, 1), Date.fromYmd(2025, 1, 1));
    try std.testing.expect(result != null);
    // 120/100 - 1 = 0.20
    try std.testing.expectApproxEqAbs(@as(f64, 0.20), result.?.total_return, 0.001);
}

test "total return with dividends -- single dividend" {
    // Stock at $100, pays $2 dividend, price stays $100.
    // Without reinvestment: 0% return.
    // With reinvestment: $2/$100 = 0.02 extra shares -> 1.02 * $100 / $100 - 1 = 2%
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2024, 1, 2), 100),
        makeCandle(Date.fromYmd(2024, 3, 15), 100),
        makeCandle(Date.fromYmd(2024, 12, 31), 100),
    };
    const divs = [_]Dividend{
        .{ .ex_date = Date.fromYmd(2024, 3, 15), .amount = 2.0 },
    };
    const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2024, 1, 1), Date.fromYmd(2025, 1, 1));
    try std.testing.expect(result != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.02), result.?.total_return, 0.0001);
}

test "total return with dividends -- quarterly dividends" {
    // Stock at $100 all year, pays $1 quarterly. Each $1 reinvested at $100 = 0.01 shares.
    // After Q1: 1.01 shares
    // After Q2: 1.01 + 1.01*1/100 = 1.01 * 1.01 = 1.0201
    // After Q3: 1.0201 * 1.01 = 1.030301
    // After Q4: 1.030301 * 1.01 = 1.04060401
    // Total return: 4.06%
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2024, 1, 2), 100),
        makeCandle(Date.fromYmd(2024, 3, 15), 100),
        makeCandle(Date.fromYmd(2024, 6, 14), 100),
        makeCandle(Date.fromYmd(2024, 9, 13), 100),
        makeCandle(Date.fromYmd(2024, 12, 13), 100),
        makeCandle(Date.fromYmd(2024, 12, 31), 100),
    };
    const divs = [_]Dividend{
        .{ .ex_date = Date.fromYmd(2024, 3, 15), .amount = 1.0 },
        .{ .ex_date = Date.fromYmd(2024, 6, 14), .amount = 1.0 },
        .{ .ex_date = Date.fromYmd(2024, 9, 13), .amount = 1.0 },
        .{ .ex_date = Date.fromYmd(2024, 12, 13), .amount = 1.0 },
    };
    const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2024, 1, 1), Date.fromYmd(2025, 1, 1));
    try std.testing.expect(result != null);
    // (1.01)^4 - 1 = 0.04060401
    try std.testing.expectApproxEqAbs(@as(f64, 0.04060401), result.?.total_return, 0.0001);
}

test "total return with dividends -- price growth plus dividends" {
    // Start $100, end $120 (20% price return).
    // One $3 dividend at mid-year when price is $110.
    // Shares: 1 + 3/110 = 1.027273
    // Final value: 1.027273 * 120 = 123.2727
    // Total return: 123.2727 / 100 - 1 = 23.27%
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2024, 1, 2), 100),
        makeCandle(Date.fromYmd(2024, 6, 14), 110),
        makeCandle(Date.fromYmd(2024, 12, 31), 120),
    };
    const divs = [_]Dividend{
        .{ .ex_date = Date.fromYmd(2024, 6, 14), .amount = 3.0 },
    };
    const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2024, 1, 1), Date.fromYmd(2025, 1, 1));
    try std.testing.expect(result != null);
    const expected = (1.0 + 3.0 / 110.0) * 120.0 / 100.0 - 1.0; // 0.23272727...
    try std.testing.expectApproxEqAbs(expected, result.?.total_return, 0.0001);
}

test "annualized return -- 3 year period" {
    // 3 years: $100 -> $150. Total return = 50%.
    // Annualized = (1.50)^(1/3) - 1 = 14.47%
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2021, 1, 4), 100),
        makeCandle(Date.fromYmd(2024, 1, 2), 150),
    };
    const result = totalReturnFromAdjClose(&candles, Date.fromYmd(2021, 1, 1), Date.fromYmd(2024, 1, 3));
    try std.testing.expect(result != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.50), result.?.total_return, 0.001);
    const ann = result.?.annualized_return.?;
    // (1.50)^(1/years) - 1, years ~ 3.0 (via 365.25)
    const years = Date.yearsBetween(Date.fromYmd(2021, 1, 4), Date.fromYmd(2024, 1, 2));
    const expected_ann = std.math.pow(f64, 1.50, 1.0 / years) - 1.0;
    try std.testing.expectApproxEqAbs(expected_ann, ann, 0.0001);
}

test "findNearestCandle -- exact match" {
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2024, 1, 2), 100),
        makeCandle(Date.fromYmd(2024, 1, 3), 101),
        makeCandle(Date.fromYmd(2024, 1, 4), 102),
    };
    // Forward exact
    const fwd = findNearestCandle(&candles, Date.fromYmd(2024, 1, 3), .forward).?;
    try std.testing.expect(fwd.date.eql(Date.fromYmd(2024, 1, 3)));
    // Backward exact
    const bwd = findNearestCandle(&candles, Date.fromYmd(2024, 1, 3), .backward).?;
    try std.testing.expect(bwd.date.eql(Date.fromYmd(2024, 1, 3)));
}

test "findNearestCandle -- weekend snap" {
    // Jan 4 2025 is Saturday, Jan 5 is Sunday
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2025, 1, 3), 100), // Friday
        makeCandle(Date.fromYmd(2025, 1, 6), 101), // Monday
    };
    // Forward from Saturday -> Monday
    const fwd = findNearestCandle(&candles, Date.fromYmd(2025, 1, 4), .forward).?;
    try std.testing.expect(fwd.date.eql(Date.fromYmd(2025, 1, 6)));
    // Backward from Saturday -> Friday
    const bwd = findNearestCandle(&candles, Date.fromYmd(2025, 1, 4), .backward).?;
    try std.testing.expect(bwd.date.eql(Date.fromYmd(2025, 1, 3)));
}

test "month-end trailing returns -- date windowing" {
    // Verify month-end logic uses correct reference dates.
    // "Today" = 2026-02-15, prior month end = 2026-01-31
    // 1yr window: 2025-01-31 to 2026-01-31
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2025, 1, 31), 100), // Jan 31 2025 is Friday
        makeCandle(Date.fromYmd(2025, 7, 1), 110),
        makeCandle(Date.fromYmd(2026, 1, 30), 120), // Jan 31 is Sat, trading day is 30th
        makeCandle(Date.fromYmd(2026, 2, 14), 125),
    };
    const today = Date.fromYmd(2026, 2, 15);
    const ret = trailingReturnsMonthEnd(&candles, today);
    // Month-end = Jan 31 2026. backward snap -> Jan 30.
    // Start = Jan 31 2025 (exact match, backward snap). End = Jan 30 2026.
    // Return = 120/100 - 1 = 20%
    try std.testing.expect(ret.one_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.20), ret.one_year.?.total_return, 0.001);
}

test "month-end trailing returns -- weekend start snaps backward" {
    // When the start month-end falls on a weekend, it should snap BACKWARD
    // to the last trading day (Friday), not forward to Monday.
    // This matches Morningstar's "last business day of the month" convention.
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2016, 1, 29), 100), // Friday (last biz day of Jan 2016)
        makeCandle(Date.fromYmd(2016, 2, 1), 95), // Monday (NOT what we want)
        makeCandle(Date.fromYmd(2026, 1, 30), 240), // End: Friday (last biz day of Jan 2026)
    };
    // Jan 31 2016 is Sunday. Backward snap -> Jan 29 (Friday).
    // Jan 31 2026 is Saturday. Backward snap -> Jan 30 (Friday).
    // Return = 240/100 - 1 = 140%
    const today = Date.fromYmd(2026, 2, 15);
    const ret = trailingReturnsMonthEnd(&candles, today);
    try std.testing.expect(ret.ten_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, 1.40), ret.ten_year.?.total_return, 0.001);
    // Verify start date is Jan 29 (Friday), not Feb 1 (Monday)
    try std.testing.expect(ret.ten_year.?.from.eql(Date.fromYmd(2016, 1, 29)));
}

test "dividends outside window are excluded" {
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2024, 1, 2), 100),
        makeCandle(Date.fromYmd(2024, 6, 14), 100),
        makeCandle(Date.fromYmd(2024, 12, 31), 100),
    };
    const divs = [_]Dividend{
        .{ .ex_date = Date.fromYmd(2023, 12, 15), .amount = 5.0 }, // before window
        .{ .ex_date = Date.fromYmd(2024, 6, 14), .amount = 2.0 }, // inside
        .{ .ex_date = Date.fromYmd(2025, 3, 15), .amount = 5.0 }, // after window
    };
    const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2024, 1, 1), Date.fromYmd(2025, 1, 1));
    try std.testing.expect(result != null);
    // Only the $2 mid-year dividend counts: 2/100 = 2%
    try std.testing.expectApproxEqAbs(@as(f64, 0.02), result.?.total_return, 0.0001);
}

test "zero price candle returns null" {
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2024, 1, 2), 0),
        makeCandle(Date.fromYmd(2024, 12, 31), 100),
    };
    const result = totalReturnFromAdjClose(&candles, Date.fromYmd(2024, 1, 1), Date.fromYmd(2025, 1, 1));
    try std.testing.expect(result == null);
}

test "empty candles returns null" {
    const candles = [_]Candle{};
    const result = totalReturnFromAdjClose(&candles, Date.fromYmd(2024, 1, 1), Date.fromYmd(2025, 1, 1));
    try std.testing.expect(result == null);
}

fn makeCandle(date: Date, price: f64) Candle {
    return .{ .date = date, .open = price, .high = price, .low = price, .close = price, .adj_close = price, .volume = 1000 };
}

// Morningstar reference data, captured 2026-02-24.
//
// AMZN Trailing Returns (as-of-date, from morningstar.com/stocks/xnas/amzn/trailing-returns):
//   Day end 2026-02-24:  1yr=-1.95%  3yr=30.66%  5yr=5.71%  10yr=22.37%
//   AMZN has no dividends, so price return = total return.
//
// VTI Trailing Returns (as-of-date, from morningstar.com/etfs/arcx/vti/trailing-returns):
//   Day end 2026-02-24:  1yr=16.62%  3yr=21.01%  5yr=12.03%  10yr=15.10% (price)
//
// VTI Performance (month-end, from morningstar.com/etfs/arcx/vti/performance):
//   Month-end Jan 31:    10yr total=15.10%  3yr total=20.20% (NAV ~20.24%)

test "as-of-date trailing returns -- AMZN vs Morningstar" {
    // Real AMZN split-adjusted closing prices from Twelve Data.
    // AMZN pays no dividends, so adj_close == close.
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2016, 2, 24), 27.702), // 10yr start
        makeCandle(Date.fromYmd(2021, 2, 24), 157.9765), // 5yr start
        makeCandle(Date.fromYmd(2023, 2, 24), 93.50), // 3yr start
        makeCandle(Date.fromYmd(2025, 2, 24), 212.71), // 1yr start
        makeCandle(Date.fromYmd(2026, 2, 24), 208.56), // end (latest close)
    };

    const ret = trailingReturns(&candles);

    // 1yr: 208.56 / 212.71 - 1 = -1.95%
    try std.testing.expect(ret.one_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, -0.0195), ret.one_year.?.total_return, 0.001);

    // 3yr: annualized. Morningstar shows 30.66%.
    try std.testing.expect(ret.three_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.3066), ret.three_year.?.annualized_return.?, 0.002);

    // 5yr: annualized. Morningstar shows 5.71%.
    try std.testing.expect(ret.five_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.0571), ret.five_year.?.annualized_return.?, 0.002);

    // 10yr: annualized. Morningstar shows 22.37%.
    try std.testing.expect(ret.ten_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.2237), ret.ten_year.?.annualized_return.?, 0.002);
}

test "as-of-date vs month-end -- different results from same data" {
    // Demonstrates that as-of-date and month-end give different results
    // when the latest close differs significantly from the month-end close.
    //
    // "Today" = 2026-02-25, month-end = Jan 31 2026
    // As-of end = Feb 24 (latest candle), month-end = Jan 30 (snap from Jan 31 Sat)
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2025, 1, 31), 100), // month-end 1yr start (Friday)
        makeCandle(Date.fromYmd(2025, 2, 24), 100), // as-of 1yr start
        makeCandle(Date.fromYmd(2025, 7, 1), 110),
        makeCandle(Date.fromYmd(2026, 1, 30), 115), // month-end end (Friday, Jan 31 is Sat)
        makeCandle(Date.fromYmd(2026, 2, 24), 120), // as-of end (latest)
    };

    // As-of-date: end=Feb 24 ($120), start=Feb 24 prior year ($100) → 20%
    const asof = trailingReturns(&candles);
    try std.testing.expect(asof.one_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.20), asof.one_year.?.total_return, 0.001);

    // Month-end: end=Jan 30 ($115), start=Jan 31 ($100) → 15%
    const me = trailingReturnsMonthEnd(&candles, Date.fromYmd(2026, 2, 25));
    try std.testing.expect(me.one_year != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.15), me.one_year.?.total_return, 0.001);
}

test "stable-NAV fund -- synthesize start candle for dividend reinvestment" {
    // Money market fund: NAV always $1, candle history only covers 3 years,
    // but dividend data goes back 5 years. Should synthesize a $1 start candle
    // and correctly compound distributions for the full 5-year period.
    //
    // Monthly $0.003 distribution on $1 NAV, 60 months:
    // shares = (1.003)^60 = 1.19668...
    // total return = 19.67%
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2023, 1, 3), 1), // candles start here (3yr)
        makeCandle(Date.fromYmd(2024, 1, 2), 1),
        makeCandle(Date.fromYmd(2025, 12, 31), 1),
    };

    // 60 monthly dividends from 2021 through 2025
    var divs: [60]Dividend = undefined;
    for (0..60) |i| {
        const month: u8 = @intCast(i % 12 + 1);
        const year: i16 = @intCast(2021 + i / 12);
        divs[i] = .{ .ex_date = Date.fromYmd(year, month, 15), .amount = 0.003 };
    }

    const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2021, 1, 1), Date.fromYmd(2026, 1, 1));
    try std.testing.expect(result != null);
    // Start date should be synthesized at the requested from date (snapped forward)
    try std.testing.expect(result.?.from.eql(Date.fromYmd(2021, 1, 1)));
    // (1.003)^60 - 1 = 0.19668
    const expected = std.math.pow(f64, 1.003, 60.0) - 1.0;
    try std.testing.expectApproxEqAbs(expected, result.?.total_return, 0.001);
}

test "stable-NAV synthesis -- non-$1 fund does not synthesize" {
    // A fund with close != $1 should NOT get a synthesized start candle.
    const candles = [_]Candle{
        makeCandle(Date.fromYmd(2023, 1, 3), 50),
        makeCandle(Date.fromYmd(2025, 12, 31), 55),
    };
    const divs = [_]Dividend{
        .{ .ex_date = Date.fromYmd(2021, 6, 15), .amount = 1.0 },
    };
    // Start date is before candle history, but close != $1 => should return null
    const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2021, 1, 1), Date.fromYmd(2026, 1, 1));
    try std.testing.expect(result == null);
}

test "withDividendFallback -- picks higher annualized return per period" {
    const d1 = Date.fromYmd(2020, 1, 1);
    const d2 = Date.fromYmd(2025, 1, 1);

    // div_returns: complete dividend data, higher returns from compounding
    const div_ret: TrailingReturns = .{
        .one_year = .{ .total_return = 0.10, .annualized_return = 0.10, .from = d1, .to = d2 },
        .three_year = .{ .total_return = 0.50, .annualized_return = 0.15, .from = d1, .to = d2 },
        .five_year = null, // dividend data too short for 5yr
        .ten_year = null, // dividend data too short for 10yr
    };
    // adj_close_returns: always available but slightly lower due to non-compounding
    const adj_ret: TrailingReturns = .{
        .one_year = .{ .total_return = 0.08, .annualized_return = 0.08, .from = d1, .to = d2 },
        .three_year = .{ .total_return = 0.60, .annualized_return = 0.17, .from = d1, .to = d2 },
        .five_year = .{ .total_return = 0.80, .annualized_return = 0.12, .from = d1, .to = d2 },
        .ten_year = .{ .total_return = 0.50, .annualized_return = 0.04, .from = d1, .to = d2 },
    };

    const merged = withDividendFallback(div_ret, adj_ret);

    // one_year: div wins (0.10 > 0.08, complete dividend data compounds better)
    try std.testing.expectApproxEqAbs(@as(f64, 0.10), merged.one_year.?.annualized_return.?, 0.001);
    // three_year: adj_close wins (0.17 > 0.15, incomplete dividend data here)
    try std.testing.expectApproxEqAbs(@as(f64, 0.17), merged.three_year.?.annualized_return.?, 0.001);
    // five_year: div null, filled from adj_close
    try std.testing.expectApproxEqAbs(@as(f64, 0.12), merged.five_year.?.annualized_return.?, 0.001);
    // ten_year: div null, filled from adj_close
    try std.testing.expectApproxEqAbs(@as(f64, 0.04), merged.ten_year.?.annualized_return.?, 0.001);
}

test "withDividendFallback -- both null stays null" {
    const a: TrailingReturns = .{};
    const b: TrailingReturns = .{};
    const merged = withDividendFallback(a, b);
    try std.testing.expect(merged.one_year == null);
    try std.testing.expect(merged.ten_year == null);
}

test "splits-only adj_close -- dividend reinvestment preferred" {
    // Simulates a TwelveData-like provider where adj_close only accounts for splits.
    // Stock pays $2/quarter dividend, price stays at ~$100.
    // adj_close shows no return (splits-only), but dividend reinvestment should
    // show the correct total return from the distributions.
    const candles = [_]Candle{
        // adj_close == close here (no splits), so adj_close return ≈ 0%
        makeCandle(Date.fromYmd(2025, 1, 2), 100),
        makeCandle(Date.fromYmd(2025, 3, 15), 100),
        makeCandle(Date.fromYmd(2025, 6, 15), 100),
        makeCandle(Date.fromYmd(2025, 9, 15), 100),
        makeCandle(Date.fromYmd(2025, 12, 31), 100),
    };
    const divs = [_]Dividend{
        .{ .ex_date = Date.fromYmd(2025, 3, 15), .amount = 2.0 },
        .{ .ex_date = Date.fromYmd(2025, 6, 15), .amount = 2.0 },
        .{ .ex_date = Date.fromYmd(2025, 9, 15), .amount = 2.0 },
        .{ .ex_date = Date.fromYmd(2025, 12, 15), .amount = 2.0 },
    };

    // adj_close return should be ~0% (price flat, no dividend adjustment)
    const adj_result = totalReturnFromAdjClose(&candles, Date.fromYmd(2025, 1, 2), Date.fromYmd(2025, 12, 31));
    try std.testing.expect(adj_result != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.0), adj_result.?.total_return, 0.01);

    // Dividend reinvestment: 3 of 4 dividends are reinvested (Dec 15 is >10 days
    // from any candle so it's skipped by snap tolerance). ~6.12% total return.
    const div_result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2025, 1, 2), Date.fromYmd(2025, 12, 31));
    try std.testing.expect(div_result != null);
    try std.testing.expect(div_result.?.total_return > 0.05);

    // When adj_close is splits-only, dividend reinvestment should be primary.
    // Wrapping in TrailingReturns to test withDividendFallback:
    const adj_tr: TrailingReturns = .{ .one_year = adj_result };
    const div_tr: TrailingReturns = .{ .one_year = div_result };

    // withDividendFallback picks the higher return for each period,
    // so dividend reinvestment wins regardless of argument order
    const total = withDividendFallback(div_tr, adj_tr);
    try std.testing.expect(total.one_year.?.total_return > 0.05);

    // Same result with reversed order — bestResult always picks higher
    const also_total = withDividendFallback(adj_tr, div_tr);
    try std.testing.expect(also_total.one_year.?.total_return > 0.05);
}

// ── Price-only return tests ─────────────────────────────────────────

test "priceReturnSnap -- no splits, simple raw close ratio" {
    // 1Y window with no splits: just (end / start) - 1
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 2), .open = 100, .high = 100, .low = 100, .close = 100, .adj_close = 95, .volume = 1000 },
        .{ .date = Date.fromYmd(2025, 1, 2), .open = 110, .high = 110, .low = 110, .close = 110, .adj_close = 110, .volume = 1000 },
    };
    const splits = [_]Split{};
    const result = priceReturnSnap(&candles, &splits, candles[0].date, candles[1].date, .forward);
    try std.testing.expect(result != null);
    // Raw close: 110 / 100 - 1 = 0.10. (NOT 110/95-1 which would be adj_close-based.)
    try std.testing.expectApproxEqAbs(@as(f64, 0.10), result.?.total_return, 0.001);
}

test "priceReturnSnap -- 10:1 split mid-window (NVDA-style)" {
    // Pre-split close: 700. Post-split close: 70. 10:1 split mid-window.
    // Two months later, post-split close: 80.
    // Provider stores unadjusted historical close ($700 pre-split,
    // $70 post-split as an actual price drop).
    // To compute price return: divide pre-split start by 10 → 70.
    // Return = 80 / 70 - 1 = 14.29%.
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 2), .open = 700, .high = 700, .low = 700, .close = 700, .adj_close = 70, .volume = 1000 },
        .{ .date = Date.fromYmd(2024, 8, 2), .open = 80, .high = 80, .low = 80, .close = 80, .adj_close = 80, .volume = 1000 },
    };
    const splits = [_]Split{
        .{ .date = Date.fromYmd(2024, 6, 10), .numerator = 10, .denominator = 1 },
    };
    const result = priceReturnSnap(&candles, &splits, candles[0].date, candles[1].date, .forward);
    try std.testing.expect(result != null);
    try std.testing.expectApproxEqAbs(@as(f64, 0.142857), result.?.total_return, 0.001);
}

test "priceReturnSnap -- split before window is ignored" {
    // Split happened BEFORE window start. The pre-split price is
    // already not in our data; we should NOT apply the ratio.
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 2), .open = 70, .high = 70, .low = 70, .close = 70, .adj_close = 70, .volume = 1000 },
        .{ .date = Date.fromYmd(2025, 1, 2), .open = 80, .high = 80, .low = 80, .close = 80, .adj_close = 80, .volume = 1000 },
    };
    const splits = [_]Split{
        // Split happened before window — must NOT be applied
        .{ .date = Date.fromYmd(2023, 6, 10), .numerator = 10, .denominator = 1 },
    };
    const result = priceReturnSnap(&candles, &splits, candles[0].date, candles[1].date, .forward);
    try std.testing.expect(result != null);
    // Expected: raw 80 / 70 - 1 = 14.29%. NOT (80 / (70/10)) - 1 = 1043%.
    try std.testing.expectApproxEqAbs(@as(f64, 0.142857), result.?.total_return, 0.001);
}

test "priceReturnSnap -- price-only differs from adj_close total return for dividend payer" {
    // KO-style: stable raw price, but adj_close drifts down due to
    // dividend payments. Price-only return must use raw close, NOT
    // adj_close (the bug we just fixed).
    const candles = [_]Candle{
        .{ .date = Date.fromYmd(2024, 1, 2), .open = 60, .high = 60, .low = 60, .close = 60, .adj_close = 56.5, .volume = 1000 }, // adj reflects ~6% in dividends paid out before this date
        .{ .date = Date.fromYmd(2025, 1, 2), .open = 65, .high = 65, .low = 65, .close = 65, .adj_close = 65, .volume = 1000 },
    };
    const splits = [_]Split{};
    const result = priceReturnSnap(&candles, &splits, candles[0].date, candles[1].date, .forward);
    try std.testing.expect(result != null);
    // Raw close: 65 / 60 - 1 = 8.33% (price only).
    // adj_close-based would be: 65 / 56.5 - 1 = 15.04% (a "total return").
    // We want the raw 8.33%.
    try std.testing.expectApproxEqAbs(@as(f64, 0.0833), result.?.total_return, 0.001);
}

test "trailingReturnsPriceOnly -- empty candles returns empty result" {
    const candles = [_]Candle{};
    const splits = [_]Split{};
    const result = trailingReturnsPriceOnly(&candles, &splits);
    try std.testing.expect(result.one_year == null);
    try std.testing.expect(result.three_year == null);
    try std.testing.expect(result.five_year == null);
    try std.testing.expect(result.ten_year == null);
}

test "trailingReturnsPriceOnly -- 1y window with no splits" {
    // Build 366 daily candles with steady appreciation 100 → 130.
    // Raw close ratio: 130/100 - 1 = 30%.
    const day_count = 366;
    var candles: [day_count]Candle = undefined;
    const start_date = Date.fromYmd(2024, 5, 19);
    for (0..day_count) |i| {
        const t: f64 = @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(day_count - 1));
        const price = 100.0 + 30.0 * t;
        candles[i] = .{
            .date = start_date.addDays(@intCast(i)),
            .open = price,
            .high = price,
            .low = price,
            .close = price,
            .adj_close = price * 0.95, // simulate dividend-adjusted (lower) for total return
            .volume = 1000,
        };
    }
    const splits = [_]Split{};
    const result = trailingReturnsPriceOnly(&candles, &splits);
    try std.testing.expect(result.one_year != null);
    // Should be ~30% (raw close), NOT ~37% (which is what adj_close would give).
    try std.testing.expectApproxEqAbs(@as(f64, 0.30), result.one_year.?.total_return, 0.01);
}

test "trailingReturnsPriceOnly -- price-only diverges from total return on dividend payer" {
    // Confirms the regression: trailingReturnsPriceOnly uses raw close,
    // trailingReturns (the existing function) uses adj_close. For a
    // dividend payer where adj_close is divergent, the two functions
    // must produce DIFFERENT results.
    const day_count = 366;
    var candles: [day_count]Candle = undefined;
    const start_date = Date.fromYmd(2024, 5, 19);
    for (0..day_count) |i| {
        candles[i] = .{
            .date = start_date.addDays(@intCast(i)),
            .open = 100,
            .high = 100,
            .low = 100,
            .close = 100,
            .adj_close = 95, // simulate ~5% dividend already baked into adj_close
            .volume = 1000,
        };
    }
    const splits = [_]Split{};
    const price_only = trailingReturnsPriceOnly(&candles, &splits);
    const total = trailingReturns(&candles); // adj_close-based

    try std.testing.expect(price_only.one_year != null);
    try std.testing.expect(total.one_year != null);

    // Price only: raw close didn't move → ~0%.
    try std.testing.expectApproxEqAbs(@as(f64, 0.0), price_only.one_year.?.total_return, 0.01);
    // Total return (adj_close): didn't move either since adj_close is constant.
    // (This data shape doesn't exercise the dividend gap; the
    // important assertion is that price_only != adj_close-based when
    // there IS a gap, which the priceReturnSnap dividend-payer test
    // above demonstrates.)
    try std.testing.expectApproxEqAbs(@as(f64, 0.0), total.one_year.?.total_return, 0.01);
}
