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

pub const ReportTime = enum {
    bmo, // before market open
    amc, // after market close
    dmh, // during market hours
    unknown,
};

/// An earnings event (historical or upcoming).
pub const EarningsEvent = struct {
    symbol: []const u8 = "",
    date: Date,
    /// Estimated EPS (analyst consensus)
    estimate: ?f64 = null,
    /// Actual reported EPS (null if upcoming)
    actual: ?f64 = null,
    /// Surprise amount (actual - estimate)
    surprise: ?f64 = null,
    /// Surprise percentage
    surprise_percent: ?f64 = null,
    /// Fiscal quarter (1-4)
    quarter: ?u8 = null,
    /// Fiscal year
    fiscal_year: ?i16 = null,
    /// Revenue actual
    revenue_actual: ?f64 = null,
    /// Revenue estimate
    revenue_estimate: ?f64 = null,
    /// When earnings are reported relative to market hours
    report_time: ReportTime = .unknown,

    pub fn isFuture(self: EarningsEvent) bool {
        return self.actual == null;
    }

    pub fn surpriseAmount(self: EarningsEvent) ?f64 {
        if (self.surprise) |s| return s;
        const act = self.actual orelse return null;
        const est = self.estimate orelse return null;
        return act - est;
    }

    pub fn surprisePct(self: EarningsEvent) ?f64 {
        if (self.surprise_percent) |s| return s;
        const act = self.actual orelse return null;
        const est = self.estimate orelse return null;
        if (est == 0) return null;
        return ((act - est) / @abs(est)) * 100.0;
    }

    /// Free any owned string fields.
    ///
    /// `symbol` is heap-allocated by both producer paths: the cache-read
    /// path dupes it into the caller's allocator (SRF `parse_allocator`,
    /// see `cache/store.zig`), and the FMP provider path dupes it in
    /// `parseEarningsResponse`. The `len > 0` guard skips the empty-string
    /// default used for in-memory/test construction, which points at a
    /// string literal rather than the heap.
    pub fn deinit(self: EarningsEvent, allocator: std.mem.Allocator) void {
        if (self.symbol.len > 0) allocator.free(self.symbol);
    }

    /// Free a slice of events, calling `deinit` on each element first.
    /// Mirror of `Dividend.freeSlice` — this is what makes
    /// `FetchResult(EarningsEvent).deinit()` release the per-event
    /// `symbol` strings instead of just the outer slice.
    pub fn freeSlice(allocator: std.mem.Allocator, events: []const EarningsEvent) void {
        for (events) |e| e.deinit(allocator);
        allocator.free(events);
    }
};

const std = @import("std");

test "isFuture" {
    const future = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 20000 }, .estimate = 1.5 };
    try std.testing.expect(future.isFuture());
    const past = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.6, .estimate = 1.5 };
    try std.testing.expect(!past.isFuture());
}

test "surpriseAmount" {
    // With surprise field set directly
    const with_surprise = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .surprise = 0.15 };
    try std.testing.expectApproxEqAbs(@as(f64, 0.15), with_surprise.surpriseAmount().?, 0.001);
    // With actual and estimate (computed)
    const computed = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.65, .estimate = 1.50 };
    try std.testing.expectApproxEqAbs(@as(f64, 0.15), computed.surpriseAmount().?, 0.001);
    // Missing actual -> null
    const no_actual = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .estimate = 1.50 };
    try std.testing.expect(no_actual.surpriseAmount() == null);
    // Missing estimate -> null
    const no_estimate = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.65 };
    try std.testing.expect(no_estimate.surpriseAmount() == null);
}

test "surprisePct" {
    // With surprise_percent set directly
    const with_pct = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .surprise_percent = 10.0 };
    try std.testing.expectApproxEqAbs(@as(f64, 10.0), with_pct.surprisePct().?, 0.001);
    // Computed: actual=1.65, estimate=1.50 -> (0.15/1.50)*100 = 10%
    const computed = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.65, .estimate = 1.50 };
    try std.testing.expectApproxEqAbs(@as(f64, 10.0), computed.surprisePct().?, 0.001);
    // Estimate = 0 -> null (division by zero guard)
    const zero_est = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.0, .estimate = 0.0 };
    try std.testing.expect(zero_est.surprisePct() == null);
    // Negative surprise: actual < estimate
    const miss = EarningsEvent{ .symbol = "AAPL", .date = Date{ .days = 19000 }, .actual = 1.35, .estimate = 1.50 };
    try std.testing.expect(miss.surprisePct().? < 0);
}

test "freeSlice releases each event's owned symbol" {
    // Mirrors the cache-read / provider path: each event's `symbol` is
    // heap-allocated and owned by the slice's allocator. freeSlice must
    // release every symbol plus the outer slice. testing.allocator fails
    // the test on any leak or double-free.
    const allocator = std.testing.allocator;
    const events = try allocator.alloc(EarningsEvent, 2);
    events[0] = .{ .symbol = try allocator.dupe(u8, "AAPL"), .date = Date{ .days = 19000 } };
    events[1] = .{ .symbol = try allocator.dupe(u8, "MSFT"), .date = Date{ .days = 19001 } };
    EarningsEvent.freeSlice(allocator, events);
}

test "deinit tolerates the empty-string default (literal, not heap)" {
    // An event built with the default symbol points at a string literal,
    // not the heap. deinit must not attempt to free it.
    const ev = EarningsEvent{ .date = Date{ .days = 19000 } };
    ev.deinit(std.testing.allocator);
}

test "freeSlice on an empty slice is a no-op" {
    EarningsEvent.freeSlice(std.testing.allocator, &.{});
}
