use std::path::PathBuf;

use betex::{
    book::BookEventEnvelope,
    disruptor::Envelope,
    engine::{root::EngineRoot, state::MarketConfig},
    error::WalError,
    types::*,
};

#[allow(dead_code)]
fn slug(input: &str) -> String {
    let mut out = String::with_capacity(input.len());
    let mut prev_dash = false;
    for ch in input.chars() {
        let c = if ch.is_ascii_alphanumeric() {
            ch.to_ascii_lowercase()
        } else {
            '-'
        };
        if c == '-' {
            if prev_dash {
                continue;
            }
            prev_dash = true;
        } else {
            prev_dash = false;
        }
        out.push(c);
    }
    let trimmed = out.trim_matches('-');
    if trimmed.is_empty() {
        "unnamed".to_string()
    } else {
        trimmed.to_string()
    }
}

#[allow(dead_code)]
pub fn unique_path_under_target(subdir: &str, test_name: &str) -> PathBuf {
    let uniq = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    let pid = std::process::id();
    // Use the OS temp directory to avoid accumulating large test artifacts in
    // the repository's `target/` tree (especially for WAL/journal specs).
    let suite = slug(subdir);
    let test = slug(test_name);
    let base = std::env::temp_dir().join("betex-tests").join(&suite);
    let _ = std::fs::create_dir_all(&base);
    base.join(format!("betex-{suite}-{test}-pid{pid}-{uniq}"))
}

#[allow(dead_code)]
pub fn is_lmdb_unsupported(err: impl std::fmt::Display) -> bool {
    err.to_string().contains("Operation not permitted")
}

#[allow(dead_code)]
pub fn assert_sequence_gap(err: &anyhow::Error, expected: u64, found: u64) {
    assert!(err.chain().any(|cause| {
        cause.downcast_ref::<WalError>().is_some_and(|wal_err| {
            matches!(
                wal_err,
                WalError::SequenceGap {
                    expected: got_expected,
                    found: got_found,
                } if *got_expected == expected && *got_found == found
            )
        })
    }));
}

#[allow(dead_code)]
pub fn assert_corrupt_record(err: &anyhow::Error, app_seq: u64) {
    assert!(err.chain().any(|cause| {
        cause.downcast_ref::<WalError>().is_some_and(
            |wal_err| matches!(wal_err, WalError::CorruptRecord { app_seq: got, .. } if *got == app_seq),
        )
    }));
}

#[allow(dead_code)]
pub fn assert_recovery_incomplete(
    err: &anyhow::Error,
    required_through: u64,
    recovered_through: u64,
) {
    assert!(err.chain().any(|cause| {
        cause.downcast_ref::<WalError>().is_some_and(|wal_err| {
            matches!(
                wal_err,
                WalError::RecoveryIncomplete {
                    required_through: got_required,
                    recovered_through: got_recovered,
                } if *got_required == required_through && *got_recovered == recovered_through
            )
        })
    }));
}

#[allow(dead_code)]
pub fn assert_invalid_tx_framing(
    err: &anyhow::Error,
    app_seq: u64,
    tx_id: u64,
    tx_len: u16,
    tx_ix: u16,
) {
    assert!(err.chain().any(|cause| {
        cause.downcast_ref::<WalError>().is_some_and(|wal_err| {
            matches!(
                wal_err,
                WalError::InvalidTxFraming {
                    app_seq: got_app_seq,
                    tx_id: got_tx_id,
                    tx_len: got_tx_len,
                    tx_ix: got_tx_ix,
                } if *got_app_seq == app_seq
                    && *got_tx_id == tx_id
                    && *got_tx_len == tx_len
                    && *got_tx_ix == tx_ix
            )
        })
    }));
}

#[allow(dead_code)]
pub fn assert_incomplete_tx_after_cursor(
    err: &anyhow::Error,
    after_seq: u64,
    tx_id: u64,
    next_expected_seq: u64,
) {
    assert!(err.chain().any(|cause| {
        cause.downcast_ref::<WalError>().is_some_and(|wal_err| {
            matches!(
                wal_err,
                WalError::IncompleteTxAfterCursor {
                    after_seq: got_after_seq,
                    tx_id: got_tx_id,
                    next_expected_seq: got_next_expected_seq,
                } if *got_after_seq == after_seq
                    && *got_tx_id == tx_id
                    && *got_next_expected_seq == next_expected_seq
            )
        })
    }));
}

#[allow(dead_code)]
pub fn assert_cursor_beyond_recoverable_head(
    err: &anyhow::Error,
    after_seq: u64,
    recoverable_head: u64,
) {
    assert!(err.chain().any(|cause| {
        cause.downcast_ref::<WalError>().is_some_and(|wal_err| {
            matches!(
                wal_err,
                WalError::CursorBeyondRecoverableHead {
                    after_seq: got_after_seq,
                    recoverable_head: got_recoverable_head,
                } if *got_after_seq == after_seq && *got_recoverable_head == recoverable_head
            )
        })
    }));
}

#[allow(dead_code)]
pub fn assert_schema_mismatch(err: &anyhow::Error, expected: u64, found: u64) {
    assert!(err.chain().any(|cause| {
        cause.downcast_ref::<WalError>().is_some_and(|wal_err| {
            matches!(
                wal_err,
                WalError::SchemaMismatch {
                    expected: got_expected,
                    found: got_found,
                } if *got_expected == expected && *got_found == found
            )
        })
    }));
}

#[allow(dead_code)]
pub fn apply_outcome(root: &mut EngineRoot, events: impl IntoIterator<Item = BookEventEnvelope>) {
    let start_seq = root.version();
    let tx_id = start_seq.saturating_add(1);
    let events: Vec<_> = events.into_iter().collect();
    let tx_len: u16 = events.len().try_into().unwrap_or(u16::MAX);

    for (ix, payload) in events.into_iter().enumerate() {
        let env = Envelope {
            seq: start_seq.saturating_add(1).saturating_add(ix as u64),
            payload,
            response_cb: None,
            tx_id,
            tx_len,
            tx_ix: ix.try_into().unwrap_or(u16::MAX),
        };
        root.assert_next_seq(env.seq);
        root.apply(std::slice::from_ref(&env.payload));
        debug_assert_eq!(root.version(), env.seq);
    }
}

#[allow(dead_code)]
pub fn two_runner_market_config(market_id: MarketId) -> MarketConfig {
    MarketConfig::two_runner(
        market_id,
        "Match Odds",
        RunnerId(1),
        RunnerId(2),
        MarketKind::InPlayCapable,
    )
}

#[allow(dead_code)]
pub fn binary_market_config(market_id: MarketId) -> MarketConfig {
    MarketConfig::binary_yes(
        market_id,
        "Binary Market",
        RunnerId(1),
        RunnerId(2),
        MarketKind::InPlayCapable,
        100,
    )
}

#[allow(dead_code)]
pub fn multi_runner_test_config(market_id: MarketId) -> MarketConfig {
    MarketConfig::multi_runner(
        market_id,
        "Test Market",
        [RunnerId(1)],
        MarketKind::InPlayCapable,
    )
}

#[allow(dead_code)]
pub fn is_lmdb_eprem(err: &anyhow::Error) -> bool {
    err.chain().any(|e| {
        if e.downcast_ref::<std::io::Error>()
            .and_then(|io| io.raw_os_error())
            == Some(1)
        {
            return true;
        }

        let Some(heed_err) = e.downcast_ref::<heed::Error>() else {
            return false;
        };
        match heed_err {
            heed::Error::Io(io) => io.raw_os_error() == Some(1),
            heed::Error::Mdb(mdb) => matches!(mdb, heed::MdbError::Other(1)),
            _ => false,
        }
    })
}
