//! Live end-to-end tests for `tail-fin-pcc` against `pcc.g0v.ronny.tw`
//! / `pcc-api.openfun.app` (Taiwan government procurement public data,
//! mirrored by the g0v community).
//!
//! PCC is the **anonymous-browser** flavor (Flavor 2 in
//! `docs/adding-endpoints.md`): no cookies, no login. The wrinkle vs
//! Reddit/YouTube is that the API mirror is fronted by Cloudflare's
//! JS Challenge, so each fresh stealth Chrome must navigate, run the
//! challenge, and wait for `wait_for_network_idle` before any API call
//! lands. Per-test setup is ~5-10s.
//!
//! All tests are `#[ignore]`-gated.
//!
//! Run (must be serial — Chrome user-data-dir is a singleton):
//!   RUSTFLAGS="-L /opt/homebrew/Cellar/libiconv/1.19/lib" \
//!       cargo test -p tail-fin-pcc --test pcc_live -- \
//!       --ignored --test-threads=1 --nocapture
//!
//! Pinned-fixture data:
//! - Search query `"電腦"` (computer) — perennial high-volume term in
//!   Taiwan government tenders.
//! - Company name `"中華電信"` (Chunghwa Telecom) — incumbent telecom,
//!   permanent gov contractor.
//! - Tax ID `"96979933"` — Chunghwa Telecom's 統一編號.
//!
//! ## Known adapter bugs surfaced by this bootstrap (NOT covered here)
//!
//! These were discovered during live-test bootstrap and warrant their
//! own follow-up PR. Skipping rather than fixing keeps this PR scoped
//! to "live regression net for the working surface":
//!
//! 1. **`TenderRecord.date` is always empty.** `parse_record` looks for
//!    `v.get("date")` but the live API puts the date elsewhere (likely
//!    `brief.date` or a Chinese-named field). All record-shape asserts
//!    here intentionally skip the `date` field.
//! 2. **`date(yyyymmdd)` returns 0 records** for both `"20240101"` and
//!    `"2024-01-01"` formats. Endpoint may have been changed/removed on
//!    the mirror; needs probing. No test for `date` until that's fixed.
//! 3. **`budgets()` returns an HTML error page** ("`SyntaxError: Unexpected
//!    token '<'`"). The `/api/searchallspecialbudget` endpoint appears
//!    broken on `pcc.g0v.ronny.tw`. No test for `budgets` until fixed.
//! 4. **`units()` returns an empty `Vec`** on the live mirror despite
//!    `parse_units` accepting the expected JSON-array shape. Either the
//!    `/api/unit` endpoint is gone or the response shape changed.
//!    `units()` and `unit(unit_id, page)` tests are omitted until the
//!    upstream is fixed.
//!
//! ## Quirks the tests work around (NOT bugs — adapter behaves correctly)
//!
//! - **`SearchResult.query` includes JSON-quote literals.** The API
//!   echoes `"\"電腦\""` (with escaped quotes) instead of `"電腦"`; the
//!   adapter passes that through verbatim, which is the right call.
//!   Tests use `contains(...)` so they survive both quoting styles.
//! - **Per-record `unit_id` format varies.** Most records have dotted
//!   decimals like `"3.76.5.20"`, but some legacy records use
//!   non-dotted forms. `unit_id` is always present (the helper asserts
//!   `!rec.unit_id.is_empty()`); the format itself isn't locked here.

use tail_fin_common::BrowserSession;
use tail_fin_pcc::PccClient;

const SEARCH_QUERY: &str = "電腦";
const COMPANY_NAME: &str = "中華電信";
const COMPANY_TAX_ID: &str = "96979933"; // 中華電信 統一編號

async fn fresh_session() -> BrowserSession {
    BrowserSession::builder()
        .headed(false)
        .viewport(1440, 900)
        .build()
        .await
        .expect("browser launch")
}

fn assert_tender_record_shape(rec: &tail_fin_pcc::TenderRecord) {
    // Only `title` and `unit_id` are reliably populated for every
    // record across the search endpoints. The other fields
    // (`unit_name`, `tender_api_url`, `date`, `tender_type`,
    // `companies`) come back populated for *some* records and empty
    // for others — likely because the underlying tender data has
    // optional sections, not because the parser is broken. Asserting
    // every field on every record produces flaky tests.
    assert!(!rec.title.is_empty(), "tender title empty: {rec:?}");
    assert!(!rec.unit_id.is_empty(), "tender unit_id empty: {rec:?}");
    // NB: `rec.date`, `rec.unit_name`, and `rec.unit_id` format are
    // intentionally not asserted — see "Known adapter bugs" in the
    // module docstring.
}

/// Aggregate-shape assertion run AFTER `assert_tender_record_shape`
/// on every record. Catches the case where ALL records in a result
/// have an empty optional field — that *is* a regression worth
/// flagging, even if any single record being empty is fine.
fn assert_some_records_have_full_shape(records: &[tail_fin_pcc::TenderRecord]) {
    let any_with_unit_name = records.iter().any(|r| !r.unit_name.is_empty());
    // Keep these mirror domains in sync with `PCC_DOMAINS` in `src/lib.rs`.
    // If g0v migrates the project (it has happened before), update both.
    let any_with_api_url = records.iter().any(|r| {
        r.tender_api_url.contains("openfun.app") || r.tender_api_url.contains("g0v.ronny.tw")
    });
    assert!(
        any_with_unit_name,
        "no record had a non-empty unit_name across {} records — parser regressed?",
        records.len()
    );
    assert!(
        any_with_api_url,
        "no record had a tender_api_url on a known mirror across {} records",
        records.len()
    );
}

// ---------------------------------------------------------------------------
// info — API metadata (latest/oldest date, total records)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn info_returns_metadata_with_iso8601_dates() {
    let session = fresh_session().await;
    let client = PccClient::new(session);
    let info = client.info().await.expect("info");

    // Per docs, the dataset has 14M+ records (1999 onwards). Floor at
    // 10M to give headroom while still catching "API broke and returns 0".
    assert!(
        info.total_records > 10_000_000,
        "expected 10M+ total records, got {}",
        info.total_records
    );
    // Dates are ISO 8601 with `+08:00` Asia/Taipei timezone, e.g.
    // `"2026-04-28T00:00:00+08:00"` (25 chars). Locking the format
    // catches a server-side switch to UNIX-seconds or YYYYMMDD.
    for (label, d) in &[
        ("latest_date", &info.latest_date),
        ("oldest_date", &info.oldest_date),
    ] {
        assert!(
            d.contains('T'),
            "{label} should be ISO 8601 with 'T' separator, got {:?}",
            d
        );
        assert!(
            d.len() >= 20,
            "{label} should be ≥20 chars (ISO 8601 with offset), got {} chars: {:?}",
            d.len(),
            d
        );
    }
    // PCC dataset starts in 1999 and grows daily. Lexicographic compare
    // works on ISO 8601 strings: oldest_date should sort before latest.
    // Catches a regression where oldest/latest get swapped.
    assert!(
        info.oldest_date.as_str() < info.latest_date.as_str(),
        "oldest_date should be < latest_date lexicographically, got {} vs {}",
        info.oldest_date,
        info.latest_date
    );
}

// ---------------------------------------------------------------------------
// search — three different search endpoints (title / company name / tax ID)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn search_by_title_computer_returns_records() {
    let session = fresh_session().await;
    let client = PccClient::new(session);
    let result = client.search(SEARCH_QUERY, 1).await.expect("search");

    // The API echoes back a JSON-quoted form (`"\"電腦\""`); use
    // `contains` so the test survives both quoting styles.
    assert!(
        result.query.contains(SEARCH_QUERY),
        "echoed query should contain {SEARCH_QUERY:?}, got {:?}",
        result.query
    );
    assert_eq!(result.page, 1);
    assert!(
        result.total_records > 1_000,
        "「電腦」 should match 1000+ tenders, got {}",
        result.total_records
    );
    assert!(
        !result.records.is_empty(),
        "page 1 should have records when total > 0"
    );
    for rec in &result.records {
        assert_tender_record_shape(rec);
    }
    assert_some_records_have_full_shape(&result.records);
    // Title-relevance floor: at least one record's title should
    // contain "電腦". Catches a regression where the search-by-title
    // endpoint silently widens to all tenders.
    let any_match = result
        .records
        .iter()
        .any(|r| r.title.contains(SEARCH_QUERY));
    assert!(
        any_match,
        "no record title contained {:?} — search-by-title broken?",
        SEARCH_QUERY
    );
}

#[tokio::test]
#[ignore]
async fn search_by_company_name_chunghwa_returns_records() {
    let session = fresh_session().await;
    let client = PccClient::new(session);
    let result = client
        .search_company(COMPANY_NAME, 1)
        .await
        .expect("search_company");

    assert!(result.query.contains(COMPANY_NAME));
    assert!(
        result.total_records > 100,
        "{COMPANY_NAME} should match 100+ tenders, got {}",
        result.total_records
    );
    assert!(!result.records.is_empty());
    for rec in &result.records {
        assert_tender_record_shape(rec);
    }
    assert_some_records_have_full_shape(&result.records);
}

#[tokio::test]
#[ignore]
async fn search_by_tax_id_chunghwa_returns_records() {
    let session = fresh_session().await;
    let client = PccClient::new(session);
    let result = client
        .search_id(COMPANY_TAX_ID, 1)
        .await
        .expect("search_id");

    assert!(result.query.contains(COMPANY_TAX_ID));
    assert!(
        result.total_records > 50,
        "中華電信 (tax_id={COMPANY_TAX_ID}) should match 50+ tenders, got {}",
        result.total_records
    );
    assert!(!result.records.is_empty());
    for rec in &result.records {
        assert_tender_record_shape(rec);
    }
    assert_some_records_have_full_shape(&result.records);
}

// NB: `units()` and `unit(unit_id, page)` tests intentionally omitted
// — see "Known adapter bugs" #4 in the module docstring (`units()`
// deterministically returns an empty Vec on the live mirror).
