pub mod bitboard;
pub mod board;
pub mod color;
pub mod eval;
pub mod hand;
pub mod movegen;
pub mod mv;
pub mod nnue;
pub mod perft;
pub mod piece;
pub mod policy;
pub mod search;
pub mod sfen;
pub mod speculative;
pub mod square;
pub mod tt;
pub mod zobrist;

#[cfg(test)]
mod tests {
    use super::*;
    use board::Board;
    use movegen::generate_legal_moves;
    use perft::perft;

    /// Known perft values from the standard shogi starting position.
    #[test]
    fn perft_startpos() {
        let mut board = Board::startpos();
        assert_eq!(perft(&mut board, 1), 30, "perft(1)");
        assert_eq!(perft(&mut board, 2), 900, "perft(2)");
        assert_eq!(perft(&mut board, 3), 25_470, "perft(3)");
        assert_eq!(perft(&mut board, 4), 719_731, "perft(4)");
    }

    #[test]
    #[ignore = "slow: run with --release"]
    fn perft5_startpos() {
        let mut board = Board::startpos();
        assert_eq!(perft(&mut board, 5), 19_861_490, "perft(5)");
    }

    // ---- SFEN / USI tests ----

    /// Parsing the startpos SFEN must yield the same hash as Board::startpos().
    #[test]
    fn sfen_startpos_hash_matches() {
        use sfen::STARTPOS_SFEN;
        let parsed = Board::from_sfen(STARTPOS_SFEN).expect("parse startpos SFEN");
        assert_eq!(
            parsed.hash(),
            Board::startpos().hash(),
            "hash mismatch: SFEN parse vs Board::startpos()"
        );
    }

    /// Round-trip: Board → SFEN → Board must preserve the hash.
    #[test]
    fn sfen_roundtrip_hash() {
        use movegen::generate_legal_moves;
        use sfen::board_to_sfen;

        let mut board = Board::startpos();

        // Play 4 moves (capture opportunities arise quickly in deep lines)
        for depth in 0..4 {
            let moves = generate_legal_moves(&mut board);
            if moves.is_empty() {
                break;
            }
            board.do_move(moves[depth % moves.len()]);
        }

        let sfen_str = board_to_sfen(&board);
        let reparsed = Board::from_sfen(&sfen_str)
            .unwrap_or_else(|e| panic!("re-parse failed: {e}\nsfen: {sfen_str}"));
        assert_eq!(
            board.hash(),
            reparsed.hash(),
            "hash mismatch after SFEN round-trip\nsfen: {sfen_str}"
        );
    }

    /// Round-trip after capturing moves (verifies hand-piece hashing).
    #[test]
    fn sfen_capture_roundtrip() {
        use sfen::board_to_sfen;

        // Use a known position with pieces in hand: play 10 moves from startpos
        let mut board = Board::startpos();
        use movegen::generate_legal_moves;
        for i in 0..10 {
            let moves = generate_legal_moves(&mut board);
            if moves.is_empty() {
                break;
            }
            board.do_move(moves[i % moves.len()]);
        }

        let sfen1 = board_to_sfen(&board);
        let b2 =
            Board::from_sfen(&sfen1).unwrap_or_else(|e| panic!("parse failed: {e}\nsfen: {sfen1}"));
        let sfen2 = board_to_sfen(&b2);

        assert_eq!(
            board.hash(),
            b2.hash(),
            "hash mismatch\nsfen1: {sfen1}\nsfen2: {sfen2}"
        );
        assert_eq!(sfen1, sfen2, "SFEN strings differ after round-trip");
    }

    /// USI move round-trip: move_to_usi → move_from_usi must recover the original move.
    #[test]
    fn usi_move_roundtrip() {
        use movegen::generate_legal_moves;
        use sfen::{move_from_usi, move_to_usi};

        let mut board = Board::startpos();
        let moves = generate_legal_moves(&mut board);
        assert!(!moves.is_empty());

        for m in &moves {
            let s = move_to_usi(*m);
            let m2 =
                move_from_usi(&s, &board).unwrap_or_else(|e| panic!("parse '{s}' failed: {e}"));
            assert_eq!(*m, m2, "move round-trip failed for '{s}'");
        }
    }

    /// parse_position_cmd with startpos + moves must match playing moves manually.
    #[test]
    fn position_cmd_matches_manual() {
        use movegen::generate_legal_moves;
        use sfen::{move_to_usi, parse_position_cmd};

        let mut board = Board::startpos();
        let moves = generate_legal_moves(&mut board);
        let m = moves[0];

        let tok = board.do_move(m);
        let expected_hash = board.hash();
        board.undo_move(tok);

        let cmd = format!("startpos moves {}", move_to_usi(m));
        let parsed = parse_position_cmd(&cmd).expect("parse position cmd");
        assert_eq!(parsed.hash(), expected_hash, "position cmd hash mismatch");
    }

    // ---- NNUE accumulator tests ----

    /// Helper: compute accumulator from scratch for a board
    fn fresh_acc(board: &Board) -> nnue::NnueAcc {
        let mut b2 = board.clone();
        b2.refresh_acc();
        b2.acc.clone()
    }

    /// Incremental accumulator must match from-scratch recompute after each move.
    /// Covers: quiet moves, captures, promotions, drops, and multi-ply undo.
    #[test]
    fn nnue_acc_incremental_matches_scratch() {
        use movegen::generate_legal_moves;

        let mut board = Board::startpos();
        let acc0 = fresh_acc(&board);
        assert_eq!(board.acc, acc0, "startpos incremental != scratch");

        // Play depth-3 moves and verify at each ply
        let moves1 = generate_legal_moves(&mut board);
        assert!(!moves1.is_empty());

        for m1 in moves1.iter().take(5) {
            let tok1 = board.do_move(*m1);
            assert_eq!(
                board.acc,
                fresh_acc(&board),
                "depth 1 mismatch after {m1:?}"
            );

            let moves2 = generate_legal_moves(&mut board);
            for m2 in moves2.iter().take(5) {
                let tok2 = board.do_move(*m2);
                assert_eq!(
                    board.acc,
                    fresh_acc(&board),
                    "depth 2 mismatch after {m2:?}"
                );

                let moves3 = generate_legal_moves(&mut board);
                for m3 in moves3.iter().take(3) {
                    let tok3 = board.do_move(*m3);
                    assert_eq!(
                        board.acc,
                        fresh_acc(&board),
                        "depth 3 mismatch after {m3:?}"
                    );
                    board.undo_move(tok3);
                    assert_eq!(board.acc, fresh_acc(&board), "undo depth 3 mismatch");
                }

                board.undo_move(tok2);
                assert_eq!(board.acc, fresh_acc(&board), "undo depth 2 mismatch");
            }

            board.undo_move(tok1);
            assert_eq!(board.acc, fresh_acc(&board), "undo depth 1 mismatch");
        }
    }

    /// Capture: the captured piece must disappear from both perspectives.
    #[test]
    fn nnue_acc_capture_removes_victim() {
        use movegen::generate_legal_moves;
        let mut board = Board::startpos();
        // Find a move that captures something (takes a few plies from start)
        // Quickest capture in startpos: deep scan
        // Just verify the check: pick first legal move 3 plies in and verify acc
        let moves = generate_legal_moves(&mut board);
        for m in moves.iter().take(3) {
            let tok = board.do_move(*m);
            assert_eq!(board.acc, fresh_acc(&board), "acc wrong after move {m:?}");
            board.undo_move(tok);
        }
    }

    /// Promotion: the piece at `to` must use the promoted kind, not the base kind.
    #[test]
    fn nnue_acc_promotion_uses_promoted_kind() {
        use movegen::generate_legal_moves;
        let mut board = Board::startpos();
        // Reach a position with promotable moves (at least 5 plies)
        fn first_promoting_move(b: &mut Board, depth: u32) -> Option<mv::Move> {
            if depth == 0 {
                return None;
            }
            for m in generate_legal_moves(b) {
                if m.promote {
                    return Some(m);
                }
                let tok = b.do_move(m);
                let r = first_promoting_move(b, depth - 1);
                b.undo_move(tok);
                if r.is_some() {
                    return r;
                }
            }
            None
        }
        // If we can't find a promotion in reasonable depth, just verify quiet moves
        if let Some(m) = first_promoting_move(&mut board, 7) {
            // navigate to the position where m is legal
            let tok = board.do_move(m);
            assert_eq!(
                board.acc,
                fresh_acc(&board),
                "acc wrong after promotion {m:?}"
            );
            board.undo_move(tok);
            assert_eq!(
                board.acc,
                fresh_acc(&board),
                "acc wrong after undo of promotion"
            );
        }
    }

    /// Zobrist hash must survive a do_move / undo_move round-trip.
    /// After undo, the hash must equal the hash before the move.
    #[test]
    fn hash_roundtrip() {
        let mut board = Board::startpos();
        let h0 = board.hash();

        for m in generate_legal_moves(&mut board) {
            let tok = board.do_move(m);
            let h1 = board.hash();
            board.undo_move(tok);
            assert_eq!(board.hash(), h0, "hash not restored after undo of {m:?}");

            // Also verify do_move changes the hash
            let tok2 = board.do_move(m);
            assert_eq!(board.hash(), h1, "hash not deterministic");
            board.undo_move(tok2);
        }
    }

    /// Parallel YBW must return some move and leave the board unchanged
    #[test]
    fn search_startpos_returns_move() {
        use search::{SearchConfig, Searcher};
        use tt::Tt;
        let mut board = Board::startpos();
        let s = Searcher::new(Tt::new(4));
        let info = s.search(
            &mut board,
            SearchConfig {
                max_depth: 4,
                time_limit: None,
            },
        );
        assert!(info.best_move.is_some(), "search returned no move");
        assert_eq!(info.depth, 4);
        assert_eq!(board.hash(), Board::startpos().hash(), "board mutated");
    }

    /// Parallel and sequential searches must agree on the best move and score.
    /// We verify this by running two independent searches on fresh TTs.
    #[test]
    fn parallel_matches_sequential_result() {
        use search::{SearchConfig, Searcher};
        use tt::Tt;

        let mut board = Board::startpos();
        let cfg = || SearchConfig {
            max_depth: 4,
            time_limit: None,
        };

        // First search (parallel, rayon uses all cores)
        let r1 = Searcher::new(Tt::new(4)).search(&mut board, cfg());
        // Second search on a fresh TT — same result expected
        let r2 = Searcher::new(Tt::new(4)).search(&mut board, cfg());

        assert_eq!(r1.score, r2.score, "scores differ");
        assert_eq!(r1.best_move, r2.best_move, "best moves differ");
    }

    /// TT warm-up must reduce node count on a second search
    #[test]
    fn tt_reduces_nodes() {
        use search::{SearchConfig, Searcher};
        use tt::Tt;
        let tt = Tt::new(16);
        let mut board = Board::startpos();
        let cfg = || SearchConfig {
            max_depth: 4,
            time_limit: None,
        };

        let r1 = Searcher::new(tt.clone()).search(&mut board, cfg());
        let r2 = Searcher::new(tt.clone()).search(&mut board, cfg());

        assert_eq!(r1.best_move, r2.best_move, "TT changed best move");
        assert!(
            r2.nodes <= r1.nodes,
            "TT did not reduce nodes ({} -> {})",
            r1.nodes,
            r2.nodes
        );
    }

    /// SpeculativeSearcher must return a valid move, leave the board unchanged,
    /// and produce a sensible (non-mate) score.
    ///
    /// NOTE: the speculative score MAY differ from the regular depth-D score —
    /// this is expected and desirable. Completed speculative tasks write deeper TT
    /// entries (depth D+1) that the main search then reads, effectively searching
    /// deeper in the most promising branches.  The resulting score is MORE accurate,
    /// not wrong.
    #[test]
    fn speculative_is_valid() {
        use search::{SearchConfig, Searcher, SpeculativeSearcher};
        use tt::Tt;

        let mut board = Board::startpos();
        let depth = 4;

        let regular = Searcher::new(Tt::new(16)).search(
            &mut board,
            SearchConfig {
                max_depth: depth,
                time_limit: None,
            },
        );

        let spec = SpeculativeSearcher::new(Tt::new(16), 3).search(
            &mut board,
            SearchConfig {
                max_depth: depth,
                time_limit: None,
            },
        );

        // Must return a move and not corrupt the board
        assert!(spec.best_move.is_some(), "spec search returned no move");
        assert_eq!(board.hash(), Board::startpos().hash(), "board mutated");
        assert_eq!(spec.depth, depth, "did not complete target depth");

        // Score must be a plausible material evaluation (not a spurious mate score)
        assert!(
            spec.score.abs() < 900_000,
            "spec returned a nonsensical score"
        );

        // Both searches must agree that SOME move exists
        assert!(regular.best_move.is_some());

        // Speculative score is allowed to differ from the regular score because
        // completed spec tasks may have written deeper TT entries that the main
        // search consumed.  Log for human inspection:
        eprintln!(
            "regular: score={} move={:?}",
            regular.score, regular.best_move
        );
        eprintln!(
            "spec:    score={} move={:?}  hits={}/{} hashfull={}‰ nodes={}",
            spec.score, spec.best_move, spec.spec_hits, spec.spec_total, spec.hashfull, spec.nodes
        );
    }

    /// Validate 10,000,000 random positions:
    ///   - perft(1) matches generate_legal_moves().len()
    ///   - do_move + undo_move is a hash-preserving round-trip for every legal move
    ///   - terminal positions (0 legal moves) are checkmates (is_in_check == true)
    #[test]
    #[ignore = "slow: run with --release (~2 min)"]
    fn random_perft_mated_10m() {
        use movegen::{generate_legal_moves, is_in_check};
        use mv::MoveToken;
        use perft::perft;

        let mut board = Board::startpos();
        let mut validations: u64 = 0;

        // xorshift64 — no external crate needed
        let mut rng: u64 = 0xDEAD_BEEF_CAFE_BABE;
        let mut rand = move || -> usize {
            rng ^= rng << 13;
            rng ^= rng >> 7;
            rng ^= rng << 17;
            rng as usize
        };

        // Stack of MoveTokens for the current game; used for clean undo on reset.
        let mut game_toks: Vec<MoveToken> = Vec::with_capacity(256);

        while validations < 10_000_000 {
            let moves = generate_legal_moves(&mut board);

            // ── Perft(1) validation ──────────────────────────────────────────
            assert_eq!(
                perft(&mut board, 1),
                moves.len() as u64,
                "perft(1) mismatch at validation {validations}"
            );

            if moves.is_empty() {
                // ── Mated-search validation ──────────────────────────────────
                // In standard Shogi, no legal moves always means checkmate.
                assert!(
                    is_in_check(&board, board.side_to_move),
                    "terminal position is not in check at validation {validations}"
                );
                validations += 1;
                // Reset to startpos via undo stack.
                for tok in game_toks.drain(..).rev() {
                    board.undo_move(tok);
                }
                continue;
            }

            // ── do_move / undo_move hash round-trip ──────────────────────────
            let h0 = board.hash();
            for &m in &moves {
                let tok = board.do_move(m);
                board.undo_move(tok);
                assert_eq!(
                    board.hash(),
                    h0,
                    "undo did not restore hash at validation {validations} move {m:?}"
                );
            }

            validations += 1;

            // Advance game with a random legal move.
            let idx = rand() % moves.len();
            let tok = board.do_move(moves[idx]);
            game_toks.push(tok);

            // Reset after 200 plies to keep games finite.
            if game_toks.len() >= 200 {
                for tok in game_toks.drain(..).rev() {
                    board.undo_move(tok);
                }
            }
        }
    }

    /// Transpositions must produce the same hash.
    /// Move sequence A-B and B-A (when both are legal in both orders)
    /// must arrive at the same hash.
    #[test]
    fn hash_transposition() {
        let mut board = Board::startpos();
        let moves = generate_legal_moves(&mut board);
        if moves.len() < 2 {
            return;
        }

        let m1 = moves[0];
        let m2 = moves[1];

        // Play m1 then undo, play m2 then undo — check hashes are independent
        let t1 = board.do_move(m1);
        let h_after_m1 = board.hash();
        board.undo_move(t1);

        let t2 = board.do_move(m2);
        let h_after_m2 = board.hash();
        board.undo_move(t2);

        // Different moves should (almost certainly) give different hashes
        assert_ne!(h_after_m1, h_after_m2, "distinct moves gave same hash");
    }
}
