const Date = @import("../Date.zig");

/// A stock split event.
pub const Split = struct {
    date: Date,
    /// Number of shares after the split (e.g., 4 in a 4:1 split)
    numerator: f64,
    /// Number of shares before the split (e.g., 1 in a 4:1 split)
    denominator: f64,

    /// Returns the split ratio (e.g., 4.0 for a 4:1 split)
    pub fn ratio(self: Split) f64 {
        return self.numerator / self.denominator;
    }
};

/// Cumulative product of split ratios for splits in the window
/// `(after, through]` - start-EXCLUSIVE, end-INCLUSIVE. Returns 1.0
/// when no split falls in the window.
///
/// This is the single source of truth for the split-window walk. Two
/// call sites depend on it and must never diverge:
///   - price-return math (`analytics/performance.zig`) DIVIDES an
///     earlier-epoch price by this to bring it into the later epoch.
///   - lot effective-share enrichment (`enrichSplits` below) MULTIPLIES
///     an earlier-epoch share count by this for the same reason.
///
/// The boundary convention matches the original inline walk in
/// `performance.priceReturnSnap`: a split exactly on `after` is excluded
/// (it predates the window's opening value) and one exactly on `through`
/// is included.
pub fn cumulativeSplitRatio(splits: []const Split, after: Date, through: Date) f64 {
    var cum: f64 = 1.0;
    for (splits) |s| {
        if (s.date.lessThan(after) or s.date.eql(after)) continue; // after-exclusive
        if (through.lessThan(s.date)) continue; // through-inclusive
        cum *= s.ratio();
    }
    return cum;
}

const std = @import("std");

test "split ratio" {
    const forward = Split{ .date = Date{ .days = 19000 }, .numerator = 4, .denominator = 1 };
    try std.testing.expectApproxEqAbs(@as(f64, 4.0), forward.ratio(), 0.001);
    const reverse = Split{ .date = Date{ .days = 19000 }, .numerator = 1, .denominator = 10 };
    try std.testing.expectApproxEqAbs(@as(f64, 0.1), reverse.ratio(), 0.001);
    const two_for_one = Split{ .date = Date{ .days = 19000 }, .numerator = 2, .denominator = 1 };
    try std.testing.expectApproxEqAbs(@as(f64, 2.0), two_for_one.ratio(), 0.001);
}

test "cumulativeSplitRatio window boundaries and compounding" {
    const s2021 = Split{ .date = Date.fromYmd(2021, 7, 20), .numerator = 4, .denominator = 1 };
    const s2024 = Split{ .date = Date.fromYmd(2024, 6, 10), .numerator = 10, .denominator = 1 };
    const reverse = Split{ .date = Date.fromYmd(2023, 5, 1), .numerator = 1, .denominator = 10 };

    // No splits at all -> identity.
    try std.testing.expectApproxEqAbs(
        @as(f64, 1.0),
        cumulativeSplitRatio(&.{}, Date.fromYmd(2020, 1, 1), Date.fromYmd(2026, 1, 1)),
        0.0001,
    );

    // Single split strictly inside the window.
    try std.testing.expectApproxEqAbs(
        @as(f64, 4.0),
        cumulativeSplitRatio(&.{s2021}, Date.fromYmd(2021, 1, 1), Date.fromYmd(2022, 1, 1)),
        0.0001,
    );

    // Two splits inside -> product (bringing pre-2021 shares to today).
    try std.testing.expectApproxEqAbs(
        @as(f64, 40.0),
        cumulativeSplitRatio(&.{ s2021, s2024 }, Date.fromYmd(2020, 1, 1), Date.fromYmd(2026, 1, 1)),
        0.0001,
    );

    // Split before the window is excluded.
    try std.testing.expectApproxEqAbs(
        @as(f64, 1.0),
        cumulativeSplitRatio(&.{s2021}, Date.fromYmd(2022, 1, 1), Date.fromYmd(2026, 1, 1)),
        0.0001,
    );

    // Split after the window is excluded.
    try std.testing.expectApproxEqAbs(
        @as(f64, 1.0),
        cumulativeSplitRatio(&.{s2024}, Date.fromYmd(2020, 1, 1), Date.fromYmd(2023, 1, 1)),
        0.0001,
    );

    // `after` boundary is EXCLUSIVE: split exactly on `after` is not applied.
    try std.testing.expectApproxEqAbs(
        @as(f64, 1.0),
        cumulativeSplitRatio(&.{s2021}, Date.fromYmd(2021, 7, 20), Date.fromYmd(2026, 1, 1)),
        0.0001,
    );

    // `through` boundary is INCLUSIVE: split exactly on `through` is applied.
    try std.testing.expectApproxEqAbs(
        @as(f64, 4.0),
        cumulativeSplitRatio(&.{s2021}, Date.fromYmd(2020, 1, 1), Date.fromYmd(2021, 7, 20)),
        0.0001,
    );

    // Reverse split composes correctly.
    try std.testing.expectApproxEqAbs(
        @as(f64, 0.1),
        cumulativeSplitRatio(&.{reverse}, Date.fromYmd(2020, 1, 1), Date.fromYmd(2026, 1, 1)),
        0.0001,
    );
}
