use crate::{json, tariff, Version};

const TARIFF_JSON: &str = include_str!("../tariff.json");

/// Parse a `v2.2.1` tariff JSON, explain it, and return the rendered Markdown.
///
/// The Markdown is emitted via `tracing` so it can be eyeballed with `--nocapture`.
fn explain_v221(json: &str) -> String {
    crate::test::setup();

    let json = json::parse_object(json).unwrap();
    let tariff = tariff::build(json, Version::V221).ignore_warnings();

    let rendered = tariff::explain(&tariff)
        .expect("the tariff should be explainable")
        .ignore_warnings();

    tracing::info!("\n{rendered}\n");

    rendered
}

/// Render the bundled sample tariff so the wording can be reviewed by eye.
#[test]
fn explain_sample_tariff() {
    crate::test::setup();

    let json = json::parse_object(TARIFF_JSON).unwrap();
    let version = tariff::infer_version(json);
    let tariff = tariff::build_versioned(version.unwrap_certain()).ignore_warnings();

    let rendered = tariff::explain(&tariff)
        .expect("the sample tariff should be explainable")
        .ignore_warnings();

    tracing::info!("\n{rendered}\n");

    assert!(rendered.contains("per kWh"), "{rendered}");
    assert!(rendered.contains("**Charging time:**"), "{rendered}");
    assert!(
        rendered.contains("**Idle time (connected but not charging):**"),
        "{rendered}"
    );
    // Every component in the sample has `step_size` 1, so no billing-step note is shown.
    assert!(!rendered.contains("illed in"), "{rendered}");
}

/// Two charging-time elements should read as a bulleted tier list: a bounded first tier followed by
/// a catch-all "for the remaining charging time" tier.
#[test]
fn explain_tiered_charging_time() {
    let rendered = explain_v221(
        r#"{
            "id": "tiered", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 10800 },
                    "price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "TIME", "price": 0.75, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- For the first 3 hours: €0.50 per hour"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- For the remaining charging time: €0.75 per hour"),
        "{rendered}"
    );
}

/// Energy tiers are bounded by consumed kWh rather than by duration.
#[test]
fn explain_energy_tiers_by_kwh() {
    let rendered = explain_v221(
        r#"{
            "id": "energy-tiers", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_kwh": 20 },
                    "price_components": [{ "type": "ENERGY", "price": 0.30, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.45, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- For the first 20 kWh: €0.30 per kWh"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- For the remaining energy: €0.45 per kWh"),
        "{rendered}"
    );
}

/// A zero rate is narrated as "free" rather than "0.00 per hour".
#[test]
fn explain_free_then_paid_idle_time() {
    let rendered = explain_v221(
        r#"{
            "id": "free-idle", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 14400 },
                    "price_components": [{ "type": "PARKING_TIME", "price": 0, "step_size": 60 }]
                },
                {
                    "price_components": [{ "type": "PARKING_TIME", "price": 3.00, "step_size": 60 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- For the first 4 hours: free"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- For the remaining idle time: €3.00 per hour"),
        "{rendered}"
    );
}

/// A time window whose end is before its start wraps past midnight.
#[test]
fn explain_wrapping_night_window() {
    let rendered = explain_v221(
        r#"{
            "id": "night", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "start_time": "23:00", "end_time": "07:00" },
                    "price_components": [{ "type": "ENERGY", "price": 0.20, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("between 23:00 and 07:00 the next day, €0.20 per kWh"),
        "{rendered}"
    );
}

/// A free tier never carries a billing-step note, even when sibling tiers have a different step.
#[test]
fn explain_free_tier_has_no_step_note() {
    let rendered = explain_v221(
        r#"{
            "id": "free-step", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 3600 },
                    "price_components": [{ "type": "PARKING_TIME", "price": 0, "step_size": 60 }]
                },
                {
                    "price_components": [{ "type": "PARKING_TIME", "price": 5.00, "step_size": 300 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- For the first 1 hour: free"),
        "{rendered}"
    );
    assert!(!rendered.contains("free (billed in"), "{rendered}");
    assert!(
        rendered.contains("(billed in 300-second steps, rounded up)"),
        "{rendered}"
    );
}

/// An element whose start and end times are equal has an empty window and never applies; it is
/// narrated as "never" rather than a misleading "between 08:00 and 08:00".
#[test]
fn explain_equal_start_end_time_is_never() {
    let rendered = explain_v221(
        r#"{
            "id": "never", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "start_time": "08:00", "end_time": "08:00" },
                    "price_components": [{ "type": "ENERGY", "price": 0.50, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("never (its window 08:00 to 08:00 is empty)"),
        "{rendered}"
    );
    assert!(!rendered.contains("between 08:00 and 08:00"), "{rendered}");
}

/// A flat fee restricted to weekend days lists those days.
#[test]
fn explain_weekday_restricted_flat_fee() {
    let rendered = explain_v221(
        r#"{
            "id": "weekend", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "day_of_week": ["SATURDAY", "SUNDAY"] },
                    "price_components": [{ "type": "FLAT", "price": 1.50, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("**Flat fee:** on Saturday, Sunday, €1.50 per session"),
        "{rendered}"
    );
}

/// A small nonzero rate must not collapse to the currency-zero string and read as free.
#[test]
fn explain_small_rate_is_not_shown_as_zero() {
    let rendered = explain_v221(
        r#"{
            "id": "tiny", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.004, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("**Energy:** €0.004 per kWh"),
        "{rendered}"
    );
    assert!(!rendered.contains("€0.00 per kWh"), "{rendered}");
}

/// Several FLAT tiers under different conditions are all shown, not just the first.
#[test]
fn explain_multiple_flat_tiers() {
    let rendered = explain_v221(
        r#"{
            "id": "flat-tiers", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "end_time": "22:00" },
                    "price_components": [{ "type": "FLAT", "price": 1.00, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "FLAT", "price": 2.50, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- Before 22:00: €1.00 per session"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- Otherwise: €2.50 per session"),
        "{rendered}"
    );
}

/// A flat fee gated by a consumption bound (`max_duration`) surfaces that condition, rather than
/// being explained as an unconditional fee.
#[test]
fn explain_flat_fee_gated_by_duration() {
    let rendered = explain_v221(
        r#"{
            "id": "flat-dur", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 3600 },
                    "price_components": [{ "type": "FLAT", "price": 1.00, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("**Flat fee:** for the first 1 hour, €1.00 per session"),
        "{rendered}"
    );
}

/// A VAT percentage on a price component is reported; the overall `min/max` bounds and a VAT
/// inclusive `min_price` are narrated too.
#[test]
fn explain_vat_and_price_bounds() {
    let rendered = explain_v221(
        r#"{
            "id": "bounds", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "min_price": { "excl_vat": 1.00, "incl_vat": 1.21 },
            "max_price": { "excl_vat": 50.00, "incl_vat": 60.50 },
            "elements": [
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.25, "vat": 21.0, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("€0.25 per kWh (excl. 21 VAT)"),
        "{rendered}"
    );
    assert!(
        rendered.contains("at least €1.00 (€1.21 incl. VAT)"),
        "{rendered}"
    );
    assert!(
        rendered.contains("never costs more than €50.00 (€60.50 incl. VAT)"),
        "{rendered}"
    );
}

/// The tariff's own validity window is narrated.
#[test]
fn explain_validity_window() {
    let rendered = explain_v221(
        r#"{
            "id": "seasonal", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "start_date_time": "2024-01-01T00:00:00Z",
            "end_date_time": "2024-12-31T23:00:00Z",
            "elements": [
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.40, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("only valid from 2024-01-01 00:00 until 2024-12-31 23:00 (UTC)"),
        "{rendered}"
    );
}

/// An element restricted to reservation sessions never applies to a charging session, so when it
/// is the only element the tariff explains as charging for nothing.
#[test]
fn explain_reservation_only_element_is_excluded() {
    let rendered = explain_v221(
        r#"{
            "id": "reservation", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "reservation": "RESERVATION" },
                    "price_components": [{ "type": "TIME", "price": 5.00, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("every element applies only to reservation sessions"),
        "{rendered}"
    );
    assert!(!rendered.contains("€5.00"), "{rendered}");
}

/// A tariff whose elements define no price components explains that, rather than blaming the tool.
#[test]
fn explain_fallback_no_price_components() {
    let rendered = explain_v221(
        r#"{
            "id": "empty", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                { "price_components": [] }
            ]
        }"#,
    );

    assert!(
        rendered.contains("none of its applicable elements define a price component"),
        "{rendered}"
    );
}

/// A tariff whose only charge is a zero flat fee explains that it is free.
#[test]
fn explain_fallback_free_flat_only() {
    let rendered = explain_v221(
        r#"{
            "id": "gratis", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                { "price_components": [{ "type": "FLAT", "price": 0, "step_size": 1 }] }
            ]
        }"#,
    );

    assert!(
        rendered.contains("is free: its only charge is a flat fee of zero"),
        "{rendered}"
    );
}

/// A catch-all tier placed before another tier for the same dimension makes the later tier
/// unreachable; the explanation drops the dead tier and says so.
#[test]
fn explain_unreachable_tier_after_catch_all() {
    let rendered = explain_v221(
        r#"{
            "id": "unreachable", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
                },
                {
                    "restrictions": { "max_duration": 10800 },
                    "price_components": [{ "type": "TIME", "price": 0.75, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("Any later tiers never apply"),
        "{rendered}"
    );
    assert!(!rendered.contains("€0.75"), "{rendered}");
}

/// When tiers disagree on the billing step, the step is described inline per tier rather than once.
#[test]
fn explain_mixed_step_size_per_tier() {
    let rendered = explain_v221(
        r#"{
            "id": "mixed-step", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 10800 },
                    "price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "TIME", "price": 0.75, "step_size": 300 }]
                }
            ]
        }"#,
    );

    // The first tier's step is 1 (continuous), so it carries no note; only the 300-second tier does.
    assert!(!rendered.contains("1-second"), "{rendered}");
    assert!(
        rendered.contains("(billed in 300-second steps, rounded up)"),
        "{rendered}"
    );
    assert!(!rendered.contains("_Billed in"), "{rendered}");
}

/// A tier gated by charging power (not by consumption) followed by a catch-all reads as
/// "Otherwise", not "for the remaining energy".
#[test]
fn explain_power_gated_tier_reads_as_otherwise() {
    let rendered = explain_v221(
        r#"{
            "id": "fast", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "min_power": 50 },
                    "price_components": [{ "type": "ENERGY", "price": 0.60, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.40, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- While charging at 50 kW or more: €0.60 per kWh"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- Otherwise: €0.40 per kWh"),
        "{rendered}"
    );
    assert!(!rendered.contains("for the remaining energy"), "{rendered}");
}

/// Only the first price component per dimension in an element is used, so a duplicate within the
/// same element is ignored rather than shown as a second tier.
#[test]
fn explain_duplicate_component_in_element_is_ignored() {
    let rendered = explain_v221(
        r#"{
            "id": "dup", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "price_components": [
                        { "type": "ENERGY", "price": 0.30, "step_size": 1 },
                        { "type": "ENERGY", "price": 0.99, "step_size": 1 }
                    ]
                }
            ]
        }"#,
    );

    assert!(rendered.contains("**Energy:** €0.30 per kWh"), "{rendered}");
    assert!(!rendered.contains("0.99"), "{rendered}");
}

/// The currency symbol comes from `mod currency`, so a non-euro tariff renders its own symbol.
#[test]
fn explain_non_euro_currency_symbol() {
    let rendered = explain_v221(
        r#"{
            "id": "usd", "country_code": "US", "party_id": "TST", "currency": "USD",
            "elements": [
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.35, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(rendered.contains("$0.35 per kWh"), "{rendered}");
}

/// A tier bounded on both ends reads as a session-duration window, followed by a catch-all.
#[test]
fn explain_banded_duration_window() {
    let rendered = explain_v221(
        r#"{
            "id": "banded", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "min_duration": 3600, "max_duration": 10800 },
                    "price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "TIME", "price": 0.75, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- Between 1 hour and 3 hours into the session: €0.50 per hour"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- For the remaining charging time: €0.75 per hour"),
        "{rendered}"
    );
}

/// A lone tier with only a lower duration bound reads as "after the first N", implying it is free
/// before then.
#[test]
fn explain_idle_charge_starts_after_delay() {
    let rendered = explain_v221(
        r#"{
            "id": "delayed-idle", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "min_duration": 14400 },
                    "price_components": [{ "type": "PARKING_TIME", "price": 2.00, "step_size": 60 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains(
            "**Idle time (connected but not charging):** after the first 4 hours, €2.00 per hour"
        ),
        "{rendered}"
    );
}

/// Several restrictions on one tier are listed together, leading the rate.
#[test]
fn explain_stacked_qualifiers() {
    let rendered = explain_v221(
        r#"{
            "id": "stacked", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": {
                        "start_time": "07:00",
                        "end_time": "19:00",
                        "day_of_week": ["MONDAY", "FRIDAY"],
                        "start_date": "2024-06-01"
                    },
                    "price_components": [{ "type": "ENERGY", "price": 0.50, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains(
            "**Energy:** between 07:00 and 19:00, on Monday, Friday, from 2024-06-01 onwards, €0.50 per kWh"
        ),
        "{rendered}"
    );
}

/// A tier can carry both a consumption bound and a qualifier; both lead the rate.
#[test]
fn explain_bound_and_qualifier_together() {
    let rendered = explain_v221(
        r#"{
            "id": "bound-and-qual", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 10800, "day_of_week": ["SATURDAY", "SUNDAY"] },
                    "price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "TIME", "price": 0.75, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- On Saturday, Sunday, for the first 3 hours: €0.50 per hour"),
        "{rendered}"
    );
}

/// Sub-hour durations are humanized into hours and minutes.
#[test]
fn explain_duration_humanized_to_hours_and_minutes() {
    let rendered = explain_v221(
        r#"{
            "id": "ninety", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 5400 },
                    "price_components": [{ "type": "TIME", "price": 0.10, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("for the first 1 hour 30 minutes, €0.10 per hour"),
        "{rendered}"
    );
}

/// A charging-time tier gated by an energy threshold (`min_kwh`) surfaces that threshold rather
/// than dropping it; the energy bound is the secondary bound on a duration dimension.
#[test]
fn explain_time_tier_surfaces_energy_threshold() {
    let rendered = explain_v221(
        r#"{
            "id": "kwh-gate", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "min_kwh": 5 },
                    "price_components": [{ "type": "TIME", "price": 0.30, "step_size": 60 }]
                },
                {
                    "price_components": [{ "type": "TIME", "price": 0, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- After the first 5 kWh: €0.30 per hour"),
        "{rendered}"
    );
}

/// An energy tier gated by a duration bound (`max_duration`) surfaces that bound; the duration
/// bound is the secondary bound on an energy dimension.
#[test]
fn explain_energy_tier_surfaces_duration_bound() {
    let rendered = explain_v221(
        r#"{
            "id": "dur-gate", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 3600 },
                    "price_components": [{ "type": "ENERGY", "price": 0.20, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.30, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- For the first 1 hour: €0.20 per kWh"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- Otherwise: €0.30 per kWh"),
        "{rendered}"
    );
}
