//! Fork resolution and DAG convergence tests.
//!
//! These tests verify:
//! - GHOSTDAG consensus fork resolution
//! - Multiple tips (DAG divergence) handling
//! - Blue/red block classification
//! - Selected parent chain convergence
//! - Reorg and chain reorganization
//! - Network partition recovery

use std::sync::Arc;
use std::time::Duration;

use tempfile::TempDir;
use tokio::time::sleep;
use tracing::info;

use synord::config::NodeConfig;
use synord::node::{NodeState, SynorNode};

/// Test timeout for operations.
#[allow(dead_code)]
const TEST_TIMEOUT: Duration = Duration::from_secs(30);

// ==================== Test Helpers ====================

/// Creates a test node configuration.
fn create_node_config(temp_dir: &TempDir, node_index: u16, seeds: Vec<String>) -> NodeConfig {
    let mut config = NodeConfig::for_network("devnet").unwrap();
    config.data_dir = temp_dir.path().join(format!("node_{}", node_index));
    config.mining.enabled = false;

    let port_base = 19000 + (std::process::id() % 500) as u16 * 10 + node_index * 3;
    config.p2p.listen_addr = format!("/ip4/127.0.0.1/tcp/{}", port_base);
    config.rpc.http_addr = format!("127.0.0.1:{}", port_base + 1);
    config.rpc.ws_addr = format!("127.0.0.1:{}", port_base + 2);
    config.p2p.seeds = seeds;

    config
}

/// Creates a mining-enabled node configuration.
#[allow(dead_code)]
fn create_miner_config(
    temp_dir: &TempDir,
    node_index: u16,
    seeds: Vec<String>,
    coinbase_addr: &str,
) -> NodeConfig {
    let mut config = create_node_config(temp_dir, node_index, seeds);
    config.mining.enabled = true;
    config.mining.coinbase_address = Some(coinbase_addr.to_string());
    config.mining.threads = 1;
    config
}

/// Test network for fork scenarios.
struct ForkTestNetwork {
    nodes: Vec<Arc<SynorNode>>,
    _temp_dirs: Vec<TempDir>,
}

impl ForkTestNetwork {
    /// Creates a network with specified number of mining nodes.
    #[allow(dead_code)]
    async fn new_with_miners(miner_count: usize) -> anyhow::Result<Self> {
        let mut temp_dirs = Vec::new();
        let mut nodes = Vec::new();

        let first_port = 19000 + (std::process::id() % 500) as u16 * 10;

        for i in 0..miner_count {
            let temp = TempDir::new()?;
            let seeds = if i == 0 {
                vec![]
            } else {
                vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)]
            };

            let coinbase = format!("tsynor1miner{}...", i);
            let config = create_miner_config(&temp, i as u16, seeds, &coinbase);
            temp_dirs.push(temp);

            let node = Arc::new(SynorNode::new(config).await?);
            nodes.push(node);
        }

        Ok(ForkTestNetwork {
            nodes,
            _temp_dirs: temp_dirs,
        })
    }

    /// Creates a standard (non-mining) network.
    async fn new(node_count: usize) -> anyhow::Result<Self> {
        let mut temp_dirs = Vec::new();
        let mut nodes = Vec::new();

        let first_port = 19000 + (std::process::id() % 500) as u16 * 10;

        for i in 0..node_count {
            let temp = TempDir::new()?;
            let seeds = if i == 0 {
                vec![]
            } else {
                vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)]
            };

            let config = create_node_config(&temp, i as u16, seeds);
            temp_dirs.push(temp);

            let node = Arc::new(SynorNode::new(config).await?);
            nodes.push(node);
        }

        Ok(ForkTestNetwork {
            nodes,
            _temp_dirs: temp_dirs,
        })
    }

    /// Starts all nodes.
    async fn start_all(&self) -> anyhow::Result<()> {
        for (i, node) in self.nodes.iter().enumerate() {
            info!(node = i, "Starting node");
            node.start().await?;
        }
        sleep(Duration::from_millis(500)).await;
        Ok(())
    }

    /// Stops all nodes.
    async fn stop_all(&self) -> anyhow::Result<()> {
        for node in &self.nodes {
            node.stop().await?;
        }
        Ok(())
    }
}

// ==================== DAG Structure Tests ====================

#[tokio::test]
async fn test_dag_tips_tracking() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    // Wait for connection
    sleep(Duration::from_secs(2)).await;

    // Check tips on each node
    for (i, node) in network.nodes.iter().enumerate() {
        let consensus = node.consensus();
        let tips: Vec<[u8; 32]> = consensus.tips().await;
        info!(node = i, tip_count = tips.len(), "DAG tips");

        // Initially should have genesis or first block as tip
        // Tips list tracks all current DAG leaves
    }

    network.stop_all().await.unwrap();
}

#[tokio::test]
async fn test_selected_parent_chain() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    // Get selected chain from each node
    for (i, node) in network.nodes.iter().enumerate() {
        let consensus = node.consensus();
        let chain: Vec<[u8; 32]> = consensus.get_selected_chain(10).await;
        info!(
            node = i,
            chain_length = chain.len(),
            "Selected parent chain"
        );

        // Chain should be consistent across nodes in same network
        for (j, block) in chain.iter().enumerate() {
            info!(
                node = i,
                position = j,
                block = hex::encode(&block[..8]),
                "Chain block"
            );
        }
    }

    network.stop_all().await.unwrap();
}

// ==================== GHOSTDAG Configuration Tests ====================

#[tokio::test]
async fn test_ghostdag_k_parameter() {
    let temp_dir = TempDir::new().unwrap();
    let config = create_node_config(&temp_dir, 0, vec![]);

    // Verify GHOSTDAG K is configured
    let ghostdag_k = config.consensus.ghostdag_k;
    info!(ghostdag_k = ghostdag_k, "GHOSTDAG K parameter");

    // K should be a reasonable value (typically 18 for devnet, higher for mainnet)
    assert!(ghostdag_k > 0, "GHOSTDAG K should be positive");
    assert!(ghostdag_k <= 64, "GHOSTDAG K should be reasonable");

    let node = SynorNode::new(config).await.unwrap();
    node.start().await.unwrap();

    // Verify K affects consensus behavior
    let consensus = node.consensus();
    // K-cluster determines how many parallel blocks are "blue"
    // Higher K = more tolerance for concurrent blocks
    let _ = consensus.current_blue_score().await;

    node.stop().await.unwrap();
}

// ==================== Blue/Red Classification Tests ====================

#[tokio::test]
async fn test_blue_score_tracking() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    // Track blue scores across nodes
    for (i, node) in network.nodes.iter().enumerate() {
        let consensus = node.consensus();
        let blue_score = consensus.current_blue_score().await;
        let daa_score = consensus.current_daa_score().await;

        info!(
            node = i,
            blue_score = blue_score,
            daa_score = daa_score,
            "Block scores"
        );

        // Blue score tracks cumulative "blueness" of chain
        // DAA score is used for difficulty adjustment
    }

    network.stop_all().await.unwrap();
}

#[tokio::test]
async fn test_block_info_blue_red_sets() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    // Get block info which includes blue/red sets
    for (i, node) in network.nodes.iter().enumerate() {
        let consensus = node.consensus();
        let tips: Vec<[u8; 32]> = consensus.tips().await;

        for tip in tips.iter().take(3) {
            if let Some(block_info) = consensus.get_block_info(tip).await {
                info!(
                    node = i,
                    block = hex::encode(&tip[..8]),
                    blue_score = block_info.blue_score,
                    blues_count = block_info.blues.len(),
                    reds_count = block_info.reds.len(),
                    parents = block_info.parents.len(),
                    children = block_info.children.len(),
                    "Block GHOSTDAG info"
                );

                // Blue set contains blocks in this block's "good" ancestry
                // Red set contains blocks that are "parallel" but not in k-cluster
            }
        }
    }

    network.stop_all().await.unwrap();
}

// ==================== Fork Scenario Tests ====================

#[tokio::test]
async fn test_concurrent_tips_handling() {
    // In GHOSTDAG, multiple tips is normal operation
    let network = ForkTestNetwork::new(3).await.unwrap();
    network.start_all().await.unwrap();

    // Wait for network to form
    sleep(Duration::from_secs(3)).await;

    // With multiple nodes, we might see multiple tips
    let mut all_tips: Vec<Vec<[u8; 32]>> = Vec::new();

    for (i, node) in network.nodes.iter().enumerate() {
        let consensus = node.consensus();
        let tips: Vec<[u8; 32]> = consensus.tips().await;
        info!(node = i, tip_count = tips.len(), "Node tips");
        all_tips.push(tips);
    }

    // In a synchronized network, tips should converge
    // But during operation, temporary divergence is expected
    info!(nodes_checked = all_tips.len(), "Tips collection complete");

    network.stop_all().await.unwrap();
}

#[tokio::test]
async fn test_chain_convergence() {
    let network = ForkTestNetwork::new(3).await.unwrap();
    network.start_all().await.unwrap();

    // Let network operate
    sleep(Duration::from_secs(3)).await;

    // Get virtual selected parent from each node
    let mut selected_parents: Vec<Option<[u8; 32]>> = Vec::new();

    for (i, node) in network.nodes.iter().enumerate() {
        let consensus = node.consensus();
        let vsp: Option<[u8; 32]> = consensus.virtual_selected_parent().await;
        info!(
            node = i,
            has_vsp = vsp.is_some(),
            vsp = vsp.map(|v| hex::encode(&v[..8])),
            "Virtual selected parent"
        );
        selected_parents.push(vsp);
    }

    // In a healthy network, selected parents should converge
    // (might temporarily differ during block propagation)
    info!(
        nodes_with_vsp = selected_parents.iter().filter(|p| p.is_some()).count(),
        "VSP convergence check"
    );

    network.stop_all().await.unwrap();
}

// ==================== Block Validation in Fork Context ====================

#[tokio::test]
async fn test_orphan_block_handling() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    // Test orphan detection (block with unknown parent)
    // This test verifies the API for block validation works
    // In a full implementation with the consensus types exported,
    // we would match on the validation result

    let consensus = network.nodes[0].consensus();
    // Create a fake block with unknown parent
    let fake_block = vec![0u8; 100]; // Invalid block bytes

    let validation = consensus.validate_block(&fake_block).await;
    info!(validation = ?validation, "Invalid block validation result");

    // The validation should indicate the block is invalid or orphan
    // We just verify the API doesn't panic

    network.stop_all().await.unwrap();
}

#[tokio::test]
async fn test_duplicate_block_rejection() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    // If we had access to an actual block, submitting it twice should
    // return Duplicate. For this test, we verify the API.
    {
        let consensus = network.nodes[0].consensus();
        // First, get a tip (existing block)
        let tips: Vec<[u8; 32]> = consensus.tips().await;
        if !tips.is_empty() {
            info!(
                tip = hex::encode(&tips[0][..8]),
                "Would test duplicate rejection"
            );
            // In full implementation, we'd serialize and resubmit
        }
    }

    network.stop_all().await.unwrap();
}

// ==================== Confirmation Depth Tests ====================

#[tokio::test]
async fn test_confirmation_counting() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    {
        let consensus = network.nodes[0].consensus();
        let tips: Vec<[u8; 32]> = consensus.tips().await;

        for tip in tips.iter().take(3) {
            let confirmations = consensus.get_confirmations(tip).await;
            info!(
                block = hex::encode(&tip[..8]),
                confirmations = confirmations,
                "Block confirmations"
            );

            // Recent tip should have 0 confirmations
            // Older blocks should have more confirmations
        }
    }

    network.stop_all().await.unwrap();
}

#[tokio::test]
async fn test_is_in_selected_chain() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    {
        let consensus = network.nodes[0].consensus();
        let tips: Vec<[u8; 32]> = consensus.tips().await;
        let chain: Vec<[u8; 32]> = consensus.get_selected_chain(10).await;

        // Check if tips are in selected chain
        for tip in tips.iter().take(2) {
            let in_chain = consensus.is_in_selected_chain(tip).await;
            info!(
                block = hex::encode(&tip[..8]),
                in_selected_chain = in_chain,
                "Selected chain membership"
            );
        }

        // Blocks in the selected chain should return true
        for block in chain.iter().take(3) {
            let in_chain = consensus.is_in_selected_chain(block).await;
            info!(
                block = hex::encode(&block[..8]),
                in_selected_chain = in_chain,
                "Chain block membership"
            );
            // These should all be true since we got them from get_selected_chain
        }
    }

    network.stop_all().await.unwrap();
}

// ==================== Finality Tests ====================

#[tokio::test]
async fn test_finality_depth_config() {
    let temp_dir = TempDir::new().unwrap();
    let config = create_node_config(&temp_dir, 0, vec![]);

    let finality_depth = config.consensus.finality_depth;
    info!(finality_depth = finality_depth, "Finality depth");

    // Finality depth determines when blocks are considered final
    // In devnet, this is typically lower for faster finality
    assert!(finality_depth > 0, "Finality depth should be positive");

    let node = SynorNode::new(config).await.unwrap();
    node.start().await.unwrap();

    // A block with confirmations >= finality_depth is considered final
    let consensus = node.consensus();
    let tips: Vec<[u8; 32]> = consensus.tips().await;
    if !tips.is_empty() {
        let confirmations = consensus.get_confirmations(&tips[0]).await;
        let is_final = confirmations >= finality_depth;
        info!(
            confirmations = confirmations,
            finality_depth = finality_depth,
            is_final = is_final,
            "Finality check"
        );
    }

    node.stop().await.unwrap();
}

// ==================== Network Partition Simulation ====================

#[tokio::test]
async fn test_partition_and_recovery() {
    // Create 3 nodes
    let temp_dirs: Vec<TempDir> = (0..3).map(|_| TempDir::new().unwrap()).collect();
    let first_port = 19000 + (std::process::id() % 500) as u16 * 10;

    // Node 0: No seeds (seed node)
    let config0 = create_node_config(&temp_dirs[0], 0, vec![]);

    // Node 1: Connects to node 0
    let config1 = create_node_config(
        &temp_dirs[1],
        1,
        vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)],
    );

    // Node 2: Connects to node 0
    let config2 = create_node_config(
        &temp_dirs[2],
        2,
        vec![format!("/ip4/127.0.0.1/tcp/{}", first_port)],
    );

    let node0 = Arc::new(SynorNode::new(config0).await.unwrap());
    let node1 = Arc::new(SynorNode::new(config1).await.unwrap());
    let node2 = Arc::new(SynorNode::new(config2).await.unwrap());

    // Start all nodes
    node0.start().await.unwrap();
    node1.start().await.unwrap();
    node2.start().await.unwrap();

    sleep(Duration::from_secs(2)).await;
    info!("Network formed with 3 nodes");

    // Simulate partition: Stop node 0 (central node)
    info!("Creating partition by stopping node 0");
    node0.stop().await.unwrap();

    sleep(Duration::from_secs(1)).await;

    // Node 1 and 2 are now partitioned (can't reach each other directly)
    // They should handle this gracefully

    {
        let net1 = node1.network();
        let peers1 = net1.peer_count().await;
        info!(peers = peers1, "Node 1 peers after partition");
    }

    {
        let net2 = node2.network();
        let peers2 = net2.peer_count().await;
        info!(peers = peers2, "Node 2 peers after partition");
    }

    // Recovery: Restart node 0
    info!("Healing partition by restarting node 0");
    // In real test, we'd need fresh config for same ports
    // For now, just verify nodes didn't crash

    assert_eq!(
        node1.state().await,
        NodeState::Running,
        "Node 1 should survive partition"
    );
    assert_eq!(
        node2.state().await,
        NodeState::Running,
        "Node 2 should survive partition"
    );

    node2.stop().await.unwrap();
    node1.stop().await.unwrap();
}

// ==================== Reward and Difficulty in Forks ====================

#[tokio::test]
async fn test_reward_calculation() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    {
        let consensus = network.nodes[0].consensus();
        let next_reward = consensus.get_next_reward().await;
        info!(reward_sompi = next_reward.as_sompi(), "Next block reward");

        // Reward should be positive
        assert!(
            next_reward.as_sompi() > 0,
            "Block reward should be positive"
        );
    }

    network.stop_all().await.unwrap();
}

#[tokio::test]
async fn test_difficulty_adjustment() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    for (i, node) in network.nodes.iter().enumerate() {
        let consensus = node.consensus();
        let difficulty = consensus.current_difficulty().await;
        let _target = consensus.get_current_target().await;

        info!(node = i, difficulty_bits = difficulty, "Difficulty info");

        // Difficulty should be set
        // Target is the hash threshold for valid blocks
    }

    network.stop_all().await.unwrap();
}

// ==================== Transaction Validation in Fork Context ====================

#[tokio::test]
async fn test_tx_validation_in_fork() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    // Test transaction validation (would need actual tx)
    let consensus = network.nodes[0].consensus();

    // Validate a dummy transaction (should fail to parse)
    let dummy_tx = vec![0u8; 50];
    let validation = consensus.validate_tx(&dummy_tx).await;

    info!(validation = ?validation, "Dummy transaction validation result");

    // The validation should indicate the transaction is invalid
    // Invalid bytes should fail to parse, which is the expected behavior
    // We verify the API doesn't panic on invalid input

    network.stop_all().await.unwrap();
}

// ==================== Block Subscriber Tests ====================

#[tokio::test]
async fn test_block_accepted_subscription() {
    let network = ForkTestNetwork::new(2).await.unwrap();
    network.start_all().await.unwrap();

    sleep(Duration::from_secs(2)).await;

    // Subscribe to block accepted events
    {
        let consensus = network.nodes[0].consensus();
        let mut rx = consensus.subscribe_blocks();

        // In production, we'd mine a block and see it here
        // For this test, verify subscription API works
        info!("Block subscription created");

        // Check if any blocks are received (unlikely in test without mining)
        match tokio::time::timeout(Duration::from_millis(500), rx.recv()).await {
            Ok(Ok(hash)) => {
                info!(
                    block = hex::encode(&hash[..8]),
                    "Received block notification"
                );
            }
            Ok(Err(_)) => {
                info!("Block channel closed");
            }
            Err(_) => {
                info!("No blocks received (expected in test without mining)");
            }
        }
    }

    network.stop_all().await.unwrap();
}
