use card_game::{Card, Game};
use klondike::{DrawStockConfig, Foundation, KlondikePile, Tableau};
use proptest::prelude::*;

use crate::game_state::GameState;

// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------

/// Collect all cards across every pile in a fixed traversal order:
/// stock → waste → foundations 1–4 → tableaux 1–7.
///
/// The order is deterministic for a given game state, so two calls on
/// equivalent states produce identical Vec outputs — the right fingerprint
/// for undo-reversibility checks.
fn all_cards(game: &GameState) -> Vec<Card> {
    let foundations = [
        Foundation::Foundation1,
        Foundation::Foundation2,
        Foundation::Foundation3,
        Foundation::Foundation4,
    ];
    let tableaux = [
        Tableau::Tableau1,
        Tableau::Tableau2,
        Tableau::Tableau3,
        Tableau::Tableau4,
        Tableau::Tableau5,
        Tableau::Tableau6,
        Tableau::Tableau7,
    ];

    let mut cards: Vec<Card> = game.stock_cards().iter().map(|(c, _)| c.clone()).collect();
    cards.extend(game.waste_cards().iter().map(|(c, _)| c.clone()));
    for f in &foundations {
        cards.extend(
            game.pile(KlondikePile::Foundation(*f))
                .iter()
                .map(|(c, _)| c.clone()),
        );
    }
    for t in &tableaux {
        cards.extend(game.pile(KlondikePile::Tableau(*t)).iter().map(|(c, _)| c.clone()));
    }
    cards
}

fn draw_mode_strategy() -> impl Strategy<Value = DrawStockConfig> {
    prop_oneof![Just(DrawStockConfig::DrawOne), Just(DrawStockConfig::DrawThree)]
}

/// Apply a sequence of random actions to a game, silently ignoring errors.
///
/// Each action is `(draw_flag, move_index)`:
/// - `draw_flag = true`  → call `game.draw()`
/// - `draw_flag = false` → pick the `move_index % len`th legal instruction
///   from `possible_instructions()` and apply it via `apply_instruction()`.
///
/// `possible_instructions()` may return `RotateStock`, which
/// `apply_instruction()` dispatches to `game.draw()`; ordinary instructions
/// are equivalent to `move_cards(from, to, count)`.
fn apply_random_actions(game: &mut GameState, actions: &[(bool, usize)]) {
    for &(do_draw, idx) in actions {
        if do_draw {
            let _ = game.draw();
        } else {
            let moves = game.possible_instructions();
            if moves.is_empty() {
                continue;
            }
            let instruction = moves[idx % moves.len()];
            let _ = game.apply_instruction(instruction);
        }
    }
}

/// Apply one move from `possible_instructions()` (or a draw if no move is
/// available), using `move_idx` to select among the legal options.
/// Returns `true` when a move was successfully applied.
fn apply_one_move(game: &mut GameState, move_idx: usize) -> bool {
    if game.is_won() {
        return false;
    }
    let moves = game.possible_instructions();
    if moves.is_empty() {
        return game.draw().is_ok();
    }
    let instruction = moves[move_idx % moves.len()];
    game.apply_instruction(instruction).is_ok()
}

// ---------------------------------------------------------------------------
// Properties
// ---------------------------------------------------------------------------

proptest! {
    /// `check_auto_complete()` and `is_win_trivial()` must agree on every
    /// reachable game state.
    ///
    /// The upstream `Klondike::is_win_trivial()` checks that the stock pile
    /// (both face-down and face-up halves) is completely empty AND that all
    /// tableau columns have no face-down cards.  Ferrous `check_auto_complete()`
    /// checks the same three conditions individually (stock empty, waste empty,
    /// all tableau cards face-up).  This property guards against any semantic
    /// drift between the two implementations so that delegating to upstream is
    /// safe.
    ///
    /// If this property ever fails, `check_auto_complete()` must NOT be fully
    /// replaced — the Ferrous conditions must be preserved and `is_win_trivial()`
    /// used only as a supplementary guard.
    #[test]
    fn check_auto_complete_agrees_with_is_win_trivial(
        seed in any::<u64>(),
        draw_mode in draw_mode_strategy(),
        actions in prop::collection::vec((any::<bool>(), 0usize..200), 0..30),
    ) {
        let mut game = GameState::new(seed, draw_mode);
        apply_random_actions(&mut game, &actions);
        prop_assert_eq!(
            game.check_auto_complete(),
            game.session().state().state().is_win_trivial(),
            "check_auto_complete() disagreed with is_win_trivial() after {:?} actions",
            actions.len(),
        );
    }

    /// `check_win()` and `is_win()` must agree on every reachable game state.
    #[test]
    fn check_win_agrees_with_is_win(
        seed in any::<u64>(),
        draw_mode in draw_mode_strategy(),
        actions in prop::collection::vec((any::<bool>(), 0usize..200), 0..30),
    ) {
        let mut game = GameState::new(seed, draw_mode);
        apply_random_actions(&mut game, &actions);
        prop_assert_eq!(
            game.check_win(),
            game.session().state().state().is_win(),
            "check_win() disagreed with is_win()",
        );
    }

    /// All 52 card IDs must be present exactly once across every pile after
    /// any reachable sequence of draw + move_cards actions.
    ///
    /// Catches two bug classes at once:
    /// - Card loss (fewer than 52 unique IDs after the sequence).
    /// - Card duplication (52 total but deduplication reduces the set).
    #[test]
    fn all_52_cards_always_present(
        seed in any::<u64>(),
        draw_mode in draw_mode_strategy(),
        actions in prop::collection::vec((any::<bool>(), 0usize..200), 0..30),
    ) {
        let mut game = GameState::new(seed, draw_mode);
        apply_random_actions(&mut game, &actions);

        let cards = all_cards(&game);
        prop_assert_eq!(cards.len(), 52, "card count ≠ 52 (got {})", cards.len());
        let unique: std::collections::HashSet<Card> = cards.iter().cloned().collect();
        prop_assert_eq!(
            unique.len(), 52,
            "duplicate cards found after dedup — a card was cloned"
        );
    }

    /// `GameState::new(seed, draw_mode)` must be deterministic: two calls
    /// with the same arguments must produce identical initial pile layouts.
    ///
    /// Pins that the deal is seeded from `seed` alone and not from any
    /// implicit source like wall-clock time or global state.
    #[test]
    fn deal_is_deterministic(
        seed in any::<u64>(),
        draw_mode in draw_mode_strategy(),
    ) {
        let a = GameState::new(seed, draw_mode);
        let b = GameState::new(seed, draw_mode);
        prop_assert_eq!(
            all_cards(&a),
            all_cards(&b),
            "same seed + draw_mode produced different deals",
        );
    }

    /// After applying any single legal move and immediately undoing it, the
    /// pile layout and move_count must be identical to their pre-move values.
    ///
    /// `setup_actions` drives the game to an arbitrary mid-game position;
    /// `move_idx` selects which legal move to apply and then undo.
    ///
    /// The score is intentionally excluded: `undo()` applies a −15 penalty
    /// that is by design, not a regression.
    #[test]
    fn undo_restores_pile_layout_and_move_count(
        seed in any::<u64>(),
        draw_mode in draw_mode_strategy(),
        setup_actions in prop::collection::vec((any::<bool>(), 0usize..200), 0..20),
        move_idx in 0usize..200,
    ) {
        let mut game = GameState::new(seed, draw_mode);
        apply_random_actions(&mut game, &setup_actions);

        // Snapshot the state before the move.
        let before_ids = all_cards(&game);
        let before_move_count = game.move_count();

        // Apply one move.
        if !apply_one_move(&mut game, move_idx) || game.is_won() {
            return Ok(()); // nothing to undo
        }

        // Undo and verify.
        prop_assert!(
            game.undo().is_ok(),
            "undo must succeed immediately after a successful move",
        );
        prop_assert_eq!(
            all_cards(&game),
            before_ids,
            "pile layout after undo differs from the pre-move snapshot",
        );
        prop_assert_eq!(
            game.move_count(),
            before_move_count,
            "move_count after undo must equal the pre-move value",
        );
    }

    /// Every move returned by `possible_instructions()` must succeed when
    /// applied via `move_cards()`.
    ///
    /// `possible_instructions()` and `move_cards()` both validate moves
    /// through the same upstream rule engine. This property ensures no
    /// drift has opened up between what the engine reports as legal and
    /// what it actually accepts.
    #[test]
    fn legal_moves_always_succeed(
        seed in any::<u64>(),
        draw_mode in draw_mode_strategy(),
        setup_actions in prop::collection::vec((any::<bool>(), 0usize..200), 0..20),
    ) {
        let mut game = GameState::new(seed, draw_mode);
        apply_random_actions(&mut game, &setup_actions);

        for instruction in game.possible_instructions() {
            // Clone so each move is tried from the same starting state.
            let mut trial = game.clone();
            let result = trial.apply_instruction(instruction);
            prop_assert!(
                result.is_ok(),
                "possible_instructions() reported {instruction:?} \
                 as legal but the call returned Err: {result:?}",
            );
        }
    }

}
