//! Regression tests for specific historical data-quality bugs.
//!
//! Each test pins the behaviour of a real, reproducible event on NEMWeb
//! that broke an earlier version of the parser. If one of these starts
//! failing it means we've regressed on a bug we already knew about —
//! not that the live API changed.
//!
//! Run with: `cargo test --test regression -- --test-threads=1 --nocapture`

use nemweb::Region;

// ── Sliding-window freshness test ─────────────────────────────────
//
// Every fixed-date test we write is a snapshot in time: data from
// 2024-01 will always pass even if AEMO changes the file format for
// 2026-04. To guarantee the regression suite catches format changes
// as soon as they happen, this test uses a date range that *moves
// forward* with the clock — three full calendar months ago, which is
// far enough in the past to be stably published in MMSDM but still
// always-current.
//
// If this test ever starts returning 0 rows, something in the
// download/parse pipeline is silently broken for recent data. That's
// exactly the failure mode we want to catch.
#[tokio::test]
async fn sliding_window_dispatch_3_months_ago() {
    // Compute "first day of the month three months ago" without
    // depending on chrono date arithmetic helpers.
    let today = chrono::Utc::now().date_naive();
    let mut y = today.year();
    let mut m = today.month() as i32 - 3;
    while m <= 0 {
        m += 12;
        y -= 1;
    }
    let from = chrono::NaiveDate::from_ymd_opt(y, m as u32, 1).unwrap();
    let to = chrono::NaiveDate::from_ymd_opt(y, m as u32, 1).unwrap();

    let from_s = from.format("%Y-%m-%d").to_string();
    let to_s = to.format("%Y-%m-%d").to_string();

    let prices = nemweb::dispatch()
        .from(&from_s)
        .unwrap()
        .to(&to_s)
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        1440,
        "Sliding-window test for {from_s}: expected 1440 dispatch prices \
         (288 × 5 regions), got {}. If this is 0, the zip_pattern regex \
         is probably not matching the current AEMO filename format.",
        prices.len()
    );
}

use chrono::Datelike;

// ── Interval-ending boundary correctness ──────────────────────────
//
// AEMO uses interval-ENDING timestamps, so the last dispatch interval
// of Jan 1 ends at 2024-01-02 00:00:00. Naive date filtering would
// drop that row from a single-day query, giving 1435 rows instead of
// the expected 1440 (and similarly losing one row per region per day
// from any multi-day range).
//
// These tests pin the fix that shifts interval-ending midnight
// timestamps back into the prior day.
#[tokio::test]
async fn boundary_single_day_dispatch_2024() {
    let prices = nemweb::dispatch()
        .from("2024-01-01")
        .unwrap()
        .to("2024-01-01")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        1440,
        "Expected 1440 dispatch prices for 1 Jan 2024 (288 × 5), got {}. \
         This count includes the interval ending 2024-01-02 00:00:00 \
         which belongs to 1 Jan's activity.",
        prices.len()
    );
}

// Year rollover — query must correctly fetch both December 2023 AND
// January 2024 MMSDM files and return the full range. This exercises
// the multi-month fetch path and ensures no off-by-one at 1 Jan.
#[tokio::test]
async fn boundary_year_rollover() {
    let prices = nemweb::dispatch()
        .from("2023-12-31")
        .unwrap()
        .to("2024-01-01")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        2880,
        "Expected 2880 dispatch prices across Dec 31 2023 → Jan 1 2024 \
         (288 × 2 days × 5 regions), got {}",
        prices.len()
    );
}

// ── MMSDM filename format change (August 2024) ───────────────────
//
// In August 2024 AEMO changed the MMSDM archive filename convention
// from `PUBLIC_DVD_<TABLE>_YYYYMM010000.zip` to
// `PUBLIC_ARCHIVE#<TABLE>#FILE01#YYYYMM010000.zip` (with `#` encoded
// as `%23` in hrefs). Queries for months from 2024-08 onward were
// silently returning zero rows until the regex was updated to match
// both conventions.
//
// This test pins the fix by querying a single day in the new-format
// era and asserting a full 1440-row result.
#[tokio::test]
async fn mmsdm_new_archive_format_oct_2024() {
    let prices = nemweb::dispatch()
        .from("2024-10-01")
        .unwrap()
        .to("2024-10-01")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        1440,
        "Expected 1440 dispatch prices for 1 Oct 2024 (new ARCHIVE filename \
         format), got {}. If this is 0, the zip_pattern regex no longer \
         matches `PUBLIC_ARCHIVE%23DISPATCHPRICE%23FILE01%23...zip`.",
        prices.len()
    );
}

// The new-format pattern must not accidentally match neighbouring
// tables like `PREDISPATCHPRICE` or `DISPATCHPRICESENSITIVITIES` whose
// zip filenames also contain the substring `DISPATCHPRICE`.
#[tokio::test]
async fn mmsdm_new_format_no_predispatch_cross_match() {
    // Query dispatch for a day covered by BOTH old and new formats —
    // if the pattern over-matches we'd see PREDISPATCHPRICE rows
    // mixed in, which would exceed 1440 × 1 = 1440 row count.
    let prices = nemweb::dispatch()
        .from("2024-09-15")
        .unwrap()
        .to("2024-09-15")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        1440,
        "Expected exactly 1440 rows; extra rows would indicate the \
         zip_pattern is matching PREDISPATCHPRICE or similar. Got {}",
        prices.len()
    );
}

// Leap-year Feb 29 must be included in 2024 queries.
#[tokio::test]
async fn boundary_leap_day() {
    let prices = nemweb::dispatch()
        .from("2024-02-29")
        .unwrap()
        .to("2024-02-29")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        1440,
        "Expected 1440 dispatch prices for 29 Feb 2024 (leap day), got {}",
        prices.len()
    );
}

// Same fix for pre-5MS trading (30-min cadence).
#[tokio::test]
async fn boundary_single_day_trading_2020() {
    let prices = nemweb::trading()
        .from("2020-01-01")
        .unwrap()
        .to("2020-01-01")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        240,
        "Expected 240 trading prices for 1 Jan 2020 pre-5MS (48 × 5), got {}",
        prices.len()
    );
}


// ── June 2022 NEM suspension — INTERVENTION row dedup ─────────────
//
// On 15 June 2022 AEMO suspended the NEM for the first time ever. For
// any dispatch interval with a market intervention, AEMO publishes two
// rows per (settlement_date, region): `INTERVENTION=0` is the
// counterfactual "what the dispatch would have been without
// intervention" run, and `INTERVENTION=1` is the actual cleared price.
//
// Consumers almost always want the actual cleared price, so the parser
// deduplicates by (settlement, region) preferring INTERVENTION=1. Without
// the dedup, a single-day query over 13 June 2022 returns ~1800 rows
// instead of the expected 1440 (288 intervals × 5 regions).
#[tokio::test]
async fn intervention_june_2022_dedup_dispatch() {
    let prices = nemweb::dispatch()
        .from("2022-06-13")
        .unwrap()
        .to("2022-06-13")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        prices.len(),
        1440,
        "Expected 1440 dispatch prices for 13 June 2022 (288 × 5), got {}",
        prices.len()
    );

    let mut seen = std::collections::HashSet::new();
    for p in &prices {
        assert!(
            seen.insert((p.settlement_date, p.region)),
            "duplicate (settlement, region) after dedup: {} @ {}",
            p.region,
            p.settlement_date
        );
    }
}

// Same event, interconnector table — DISPATCHINTERCONNECTORRES also
// carries the INTERVENTION column, so it must dedupe too.
#[tokio::test]
async fn intervention_june_2022_dedup_interconnector() {
    let flows = nemweb::interconnector()
        .from("2022-06-13")
        .unwrap()
        .to("2022-06-13")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    // 6 interconnectors × 288 intervals = 1728.
    assert_eq!(
        flows.len(),
        1728,
        "Expected 1728 interconnector flows for 13 June 2022 (288 × 6), got {}",
        flows.len()
    );

    let mut seen = std::collections::HashSet::new();
    for f in &flows {
        assert!(
            seen.insert((f.settlement_date, f.interconnector_id.clone())),
            "duplicate (settlement, interconnector_id) after dedup: {} @ {}",
            f.interconnector_id,
            f.settlement_date
        );
    }
}

// ── Market suspension flag surfaced (June 2022) ───────────────────
//
// During the NEM suspension (13-23 June 2022) AEMO published prices
// with `MARKETSUSPENDEDFLAG` set non-zero and the Administered Price
// mechanism in effect. The parser must surface these flags so users
// can filter or weight suspended intervals differently from normal
// market-clearing prices.
#[tokio::test]
async fn market_suspended_flag_surfaced_june_2022() {
    let prices = nemweb::dispatch()
        .from("2022-06-15")
        .unwrap()
        .to("2022-06-15")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    let suspended: Vec<_> = prices
        .iter()
        .filter(|p| p.market_suspended_flag != 0)
        .collect();

    assert!(
        !suspended.is_empty(),
        "Expected some market-suspended intervals on 15 June 2022, got none. \
         Either the parser is losing the flag or the flag column was renamed."
    );

    // During suspension, Administered Prices cap at a few hundred $/MWh,
    // so no suspended interval should have extreme prices.
    for p in &suspended {
        assert!(
            p.price.abs() < 5000.0,
            "Suspended interval has unexpectedly extreme price: ${} at {}",
            p.price,
            p.settlement_date
        );
    }

    println!(
        "15 June 2022: {} suspended intervals out of {} total",
        suspended.len(),
        prices.len()
    );
}

// Region demand also carries INTERVENTION.
#[tokio::test]
async fn intervention_june_2022_dedup_demand() {
    let demand = nemweb::demand()
        .from("2022-06-13")
        .unwrap()
        .to("2022-06-13")
        .unwrap()
        .fetch()
        .await
        .unwrap();

    assert_eq!(
        demand.len(),
        1440,
        "Expected 1440 region demand rows for 13 June 2022 (288 × 5), got {}",
        demand.len()
    );

    let mut seen = std::collections::HashSet::new();
    for d in &demand {
        assert!(
            seen.insert((d.settlement_date, d.region)),
            "duplicate (settlement, region) after dedup: {} @ {}",
            d.region,
            d.settlement_date
        );
    }

    // Sanity: all regions represented.
    let regions: std::collections::HashSet<Region> = demand.iter().map(|d| d.region).collect();
    assert_eq!(regions.len(), 5);
}
