struct PolicyValueTestResult
    test::PolicyValueTest
    prediction::Vector{Float32}
end

struct PolicyValueTestResultSVG
    result::PolicyValueTestResult
end

svg(x::PolicyValueTestResult) = PolicyValueTestResultSVG(x)
policypredsvg(state::AbstractState, ::PolicyValueTestResult) = @assert false OVERRIDE_MSG

function getdf(T::Type{PolicyValueTestResult}, results, limit=100)
    test_num = "Test #"
    test_pf = "P/F"
    test_result = "Pred (π*)"
    optimal_action = "Actual (π*)"
    d = Dict(
        test_num=>[],
        test_pf=>[],
        test_result=>[],
        optimal_action=>[],
    )
    for result in results
        idx = result.test.idx
        optimal = result.test.actions
        pred = result.prediction
        _, greedy_action = findmax(pred)
        pass = in(greedy_action, optimal)
        from = 1 
        to = min(limit, length(pred))
        actions = reverse(sortperm(pred))[from:to]
        prs = pred[actions]
        action_prs = map(x->begin
            action, pr = x
            pr_s = @sprintf "%.2f" pr
            "$action ($(pr_s[2:end]))"
        end, zip(actions, prs))
        result_policy = join(action_prs, ", ")
        push!(d[test_num], idx)
        push!(d[test_pf], pass ? "Pass" : "Fail")
        push!(d[test_result], result_policy)
        push!(d[optimal_action], join(optimal, ","))
    end
    DataFrame(
        test_num=>d[test_num],
        test_pf=>d[test_pf],
        test_result=>d[test_result],
        optimal_action=>d[optimal_action],
    )
end

function Base.show(io::IO, m::MIME"text/plain", x::PolicyValueTestResult)
    println()
    pretty_results(
        io,
        getdf(PolicyValueTestResult, [x]); 
        title="Policy Test Result #$(x.test.idx)"
    )
end

function Base.show(io::IO, m::MIME"image/svg+xml", x::PolicyValueTestResultSVG)
    test = x.result.test
    pred = x.result.prediction
    s = test.state
    player_name = s.to_play.name
    actions = test.actions
    state_grid = getgrid(s; header="to play: $player_name", margin_left=0)
    next_states = Rect[]
    from = 1
    len = length(actions)
    to = min(len, 3)
    for (idx,a) in enumerate(actions[from:to])
        push!(
            next_states,
            getgrid(s, a, 0f0, getnextstate(s, a); header="action: $a", margin_left=0)
        )
    end
    if length(actions) > 3
        push!(next_states, Grid(1,1, (r,c)->"..."; line_thickness=0))
    end
    input_grid = Grid(
        [state_grid], 
        :, 
        1, 
        header="INPUT", 
        margin_left=0, 
        line_thickness=0
    )
    policy_grid_header = "ACTUAL π*"
    policy_grid = Grid(
        next_states, 
        :, 
        1, 
        header=(text=policy_grid_header, height=40), 
        margin_left=0, 
        line_thickness=0,
    )
    pred_states = Rect[]
    limit = 3
    from = 1 
    to = min(limit, length(pred))
    pred_actions = reverse(sortperm(pred))[from:to]
    prs = pred[pred_actions]
    for (a,pr) in zip(pred_actions, prs)
        pr_str = @sprintf "%.2f" pr
        push!(
            pred_states,
            getgrid(
                s, 
                a, 
                0f0, 
                getnextstate(s, a); 
                margin_left=0, 
                header="action: $a",
                subheader=(text=pr_str, color="#666"),
            )
        )
    end
    pred_grid = Grid(
        pred_states, 
        :, 
        1;
        header="PRED π*",
        margin_left=0,
        line_thickness=0,
    )
    g = Grid([input_grid,pred_grid,policy_grid]; cell_valign=:top, header="POLICY TEST RESULT #$(test.idx)", line_thickness=0)
    show(io, m, g)
end