//! Generic padding wrapper for any type with a `format(self, *Writer)`
//! method.
//!
//! Wraps an inner value plus a target width and an alignment side.
//! Its own `format` method renders the inner value into a stack
//! buffer to measure its length, then writes the padded output to
//! the caller's writer.
//!
//! Comptime instantiation per inner type is essentially free: the
//! compiler emits one body per `Padded(T)` actually used, with the
//! inner `format` call inlined.
//!
//! Used by `Money.padRight`/`padLeft` and `Date.padRight`/`padLeft`,
//! plus any future format-bearing type that needs column alignment.

const std = @import("std");

pub fn Padded(comptime T: type) type {
    return struct {
        inner: T,
        width: usize,
        alignment: enum { left, right },

        const Self = @This();

        pub fn format(self: Self, w: *std.Io.Writer) std.Io.Writer.Error!void {
            // Render the inner value into a stack buffer first so
            // we can measure its length and pad. 64 bytes covers
            // every realistic format-method output in this codebase
            // (Money's worst case is ~14 chars; Date is exactly 10).
            // An inner value exceeding the buffer surfaces as
            // WriteFailed to the caller rather than UB.
            var tmp: [64]u8 = undefined;
            var fixed = std.Io.Writer.fixed(&tmp);
            try self.inner.format(&fixed);
            const text = fixed.buffered();

            const pad = if (text.len >= self.width) 0 else self.width - text.len;
            switch (self.alignment) {
                .right => {
                    try w.splatByteAll(' ', pad);
                    try w.writeAll(text);
                },
                .left => {
                    try w.writeAll(text);
                    try w.splatByteAll(' ', pad);
                },
            }
        }
    };
}

// ── Tests ──────────────────────────────────────────────────────

const testing = std.testing;

/// Simple type with a format method, used to verify Padded(T) works
/// for arbitrary inner types (not just Money / Date).
const Tag = struct {
    label: []const u8,

    pub fn format(self: Tag, w: *std.Io.Writer) std.Io.Writer.Error!void {
        try w.writeAll(self.label);
    }
};

/// Helper: render `value` via `{f}` and return an owned string. Caller frees.
fn renderAlloc(value: anytype) ![]u8 {
    var aw: std.Io.Writer.Allocating = .init(testing.allocator);
    defer aw.deinit();
    try aw.writer.print("{f}", .{value});
    return aw.toOwnedSlice();
}

test "Padded right-aligns shorter inner" {
    const padded: Padded(Tag) = .{ .inner = .{ .label = "x" }, .width = 5, .alignment = .right };
    const s = try renderAlloc(padded);
    defer testing.allocator.free(s);
    try testing.expectEqualStrings("    x", s);
}

test "Padded left-aligns shorter inner" {
    const padded: Padded(Tag) = .{ .inner = .{ .label = "x" }, .width = 5, .alignment = .left };
    const s = try renderAlloc(padded);
    defer testing.allocator.free(s);
    try testing.expectEqualStrings("x    ", s);
}

test "Padded leaves inner unchanged when wider than width" {
    const padded: Padded(Tag) = .{ .inner = .{ .label = "abcdef" }, .width = 3, .alignment = .right };
    const s = try renderAlloc(padded);
    defer testing.allocator.free(s);
    try testing.expectEqualStrings("abcdef", s);
}

test "Padded equal width emits exactly the inner with no padding" {
    const padded: Padded(Tag) = .{ .inner = .{ .label = "abc" }, .width = 3, .alignment = .right };
    const s = try renderAlloc(padded);
    defer testing.allocator.free(s);
    try testing.expectEqualStrings("abc", s);
}

test "Padded width=0 emits inner unchanged regardless of alignment" {
    const right: Padded(Tag) = .{ .inner = .{ .label = "abc" }, .width = 0, .alignment = .right };
    const left: Padded(Tag) = .{ .inner = .{ .label = "abc" }, .width = 0, .alignment = .left };
    const sr = try renderAlloc(right);
    defer testing.allocator.free(sr);
    const sl = try renderAlloc(left);
    defer testing.allocator.free(sl);
    try testing.expectEqualStrings("abc", sr);
    try testing.expectEqualStrings("abc", sl);
}

test "Padded with empty inner emits pure spaces" {
    const right: Padded(Tag) = .{ .inner = .{ .label = "" }, .width = 4, .alignment = .right };
    const left: Padded(Tag) = .{ .inner = .{ .label = "" }, .width = 4, .alignment = .left };
    const sr = try renderAlloc(right);
    defer testing.allocator.free(sr);
    const sl = try renderAlloc(left);
    defer testing.allocator.free(sl);
    try testing.expectEqualStrings("    ", sr);
    try testing.expectEqualStrings("    ", sl);
}

test "Padded composes inside a larger format string" {
    var aw: std.Io.Writer.Allocating = .init(testing.allocator);
    defer aw.deinit();
    const left: Padded(Tag) = .{ .inner = .{ .label = "id" }, .width = 4, .alignment = .left };
    const right: Padded(Tag) = .{ .inner = .{ .label = "42" }, .width = 4, .alignment = .right };
    try aw.writer.print("[{f}|{f}]", .{ left, right });
    const s = try aw.toOwnedSlice();
    defer testing.allocator.free(s);
    try testing.expectEqualStrings("[id  |  42]", s);
}

test "Padded counts bytes, not codepoints (multi-byte content overflows visually)" {
    // "é" is 2 bytes in UTF-8. With width=3 and 2-byte content, only
    // 1 byte of padding is added — correct for fixed-width terminal
    // output where the user supplies a column count, but worth pinning
    // so a future "fix" using grapheme width doesn't silently change
    // existing alignment.
    const padded: Padded(Tag) = .{ .inner = .{ .label = "é" }, .width = 3, .alignment = .right };
    const s = try renderAlloc(padded);
    defer testing.allocator.free(s);
    try testing.expectEqual(@as(usize, 3), s.len);
    try testing.expectEqualStrings(" é", s);
}

test "Padded works with Date.padLeft / Date.padRight constructors" {
    const Date = @import("Date.zig");
    const d = Date.fromYmd(2024, 1, 5);

    const right_s = try renderAlloc(d.padRight(12));
    defer testing.allocator.free(right_s);
    try testing.expectEqualStrings("  2024-01-05", right_s);

    const left_s = try renderAlloc(d.padLeft(12));
    defer testing.allocator.free(left_s);
    try testing.expectEqualStrings("2024-01-05  ", left_s);
}

test "Padded works with Money.padLeft / Money.padRight constructors" {
    const Money = @import("Money.zig");
    const m = Money.from(1234.5);

    const right_s = try renderAlloc(m.padRight(12));
    defer testing.allocator.free(right_s);
    try testing.expectEqualStrings("   $1,234.50", right_s);

    const left_s = try renderAlloc(m.padLeft(12));
    defer testing.allocator.free(left_s);
    try testing.expectEqualStrings("$1,234.50   ", left_s);
}

test "Padded composes with Money variants (whole / trim / signed)" {
    const Money = @import("Money.zig");

    const whole = try renderAlloc(Money.from(1234.0).whole().padRight(10));
    defer testing.allocator.free(whole);
    try testing.expectEqualStrings("    $1,234", whole);

    const trim = try renderAlloc(Money.from(1234.0).trim().padRight(10));
    defer testing.allocator.free(trim);
    try testing.expectEqualStrings("    $1,234", trim);

    const signed_pos = try renderAlloc(Money.from(50.0).signed().padRight(10));
    defer testing.allocator.free(signed_pos);
    try testing.expectEqualStrings("   +$50.00", signed_pos);
}
