use std::{
    fs,
    sync::{Mutex, MutexGuard, OnceLock},
};

use solana_program_pack::Pack;
use solana_pubkey::Pubkey;
use solana_rpc_client::rpc_client::RpcClient;
use solana_signer::Signer;
use spl_associated_token_account_interface::address::get_associated_token_address_with_program_id;
use spl_token_interface::state::{Account as TokenAccount, Mint};
use tempfile::tempdir;

use super::{
    Cheatcodes,
    builders::{DeployProgram, ResetAccount, SetAccount, SetTokenAccount, StreamAccount},
    resolve_target_dir, spl_token_program_id,
};
use crate::{Surfnet, error::SurfnetResult};

fn create_test_mint(cheats: &Cheatcodes<'_>, mint: &Pubkey) -> SurfnetResult<()> {
    let mut mint_data = [0u8; Mint::LEN];
    let mint_state = Mint {
        decimals: 6,
        supply: 1_000_000_000,
        is_initialized: true,
        ..Mint::default()
    };
    mint_state.pack_into_slice(&mut mint_data);

    cheats.set_account(mint, 1_461_600, &mint_data, &spl_token_program_id())
}

fn read_token_amount(cheats: &Cheatcodes<'_>, owner: &Pubkey, mint: &Pubkey) -> u64 {
    let ata = cheats.get_ata(owner, mint, None);
    let account = cheats.rpc_client().get_account(&ata).unwrap();
    let token_account = TokenAccount::unpack(&account.data).unwrap();
    token_account.amount
}

fn rpc_client(cheats: &Cheatcodes<'_>) -> RpcClient {
    cheats.rpc_client()
}

fn test_lock() -> MutexGuard<'static, ()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

fn sample_idl(program_id: &Pubkey) -> serde_json::Value {
    serde_json::json!({
        "address": program_id.to_string(),
        "metadata": {
            "name": "test_program",
            "version": "0.1.0",
            "spec": "0.1.0",
            "description": "Created with Anchor"
        },
        "instructions": [],
        "accounts": [],
        "types": [],
        "events": [],
        "errors": [],
        "constants": []
    })
}

#[tokio::test(flavor = "multi_thread")]
async fn fund_sol_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let address = Pubkey::new_unique();

    cheats.fund_sol(&address, 123_456_789).unwrap();

    let balance = cheats.rpc_client().get_balance(&address).unwrap();
    assert_eq!(balance, 123_456_789);
}

#[tokio::test(flavor = "multi_thread")]
async fn set_account_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let address = Pubkey::new_unique();
    let owner = Pubkey::new_unique();
    let data = vec![1, 2, 3, 4, 5];

    cheats
        .set_account(&address, 424_242, &data, &owner)
        .unwrap();

    let account = cheats.rpc_client().get_account(&address).unwrap();
    assert_eq!(account.lamports, 424_242);
    assert_eq!(account.owner, owner);
    assert_eq!(account.data, data);
}

#[tokio::test(flavor = "multi_thread")]
async fn set_account_builder_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let address = Pubkey::new_unique();
    let owner = Pubkey::new_unique();
    let data = vec![0xAA, 0xBB, 0xCC, 0xDD];

    cheats
        .execute(
            SetAccount::new(address)
                .lamports(808_080)
                .data(data.clone())
                .owner(owner)
                .rent_epoch(42)
                .executable(false),
        )
        .unwrap();

    let account = cheats.rpc_client().get_account(&address).unwrap();
    assert_eq!(account.lamports, 808_080);
    assert_eq!(account.owner, owner);
    assert_eq!(account.data, data);
    assert_eq!(account.rent_epoch, 42);
    assert!(!account.executable);
}

#[tokio::test(flavor = "multi_thread")]
async fn fund_sol_many_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let first = Pubkey::new_unique();
    let second = Pubkey::new_unique();

    cheats
        .fund_sol_many(&[(&first, 10_000), (&second, 20_000)])
        .unwrap();

    assert_eq!(cheats.rpc_client().get_balance(&first).unwrap(), 10_000);
    assert_eq!(cheats.rpc_client().get_balance(&second).unwrap(), 20_000);
}

#[tokio::test(flavor = "multi_thread")]
async fn fund_token_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let owner = Pubkey::new_unique();
    let mint = Pubkey::new_unique();

    create_test_mint(&cheats, &mint).unwrap();
    cheats.fund_token(&owner, &mint, 55, None).unwrap();

    assert_eq!(read_token_amount(&cheats, &owner, &mint), 55);
}

#[tokio::test(flavor = "multi_thread")]
async fn set_token_account_builder_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let owner = Pubkey::new_unique();
    let mint = Pubkey::new_unique();

    create_test_mint(&cheats, &mint).unwrap();
    cheats
        .execute(SetTokenAccount::new(owner, mint).amount(321))
        .unwrap();

    assert_eq!(read_token_amount(&cheats, &owner, &mint), 321);
}

#[tokio::test(flavor = "multi_thread")]
async fn set_token_balance_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let owner = Pubkey::new_unique();
    let mint = Pubkey::new_unique();

    create_test_mint(&cheats, &mint).unwrap();
    cheats.fund_token(&owner, &mint, 10, None).unwrap();
    cheats.set_token_balance(&owner, &mint, 777, None).unwrap();

    assert_eq!(read_token_amount(&cheats, &owner, &mint), 777);
}

#[tokio::test(flavor = "multi_thread")]
async fn fund_token_many_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let mint = Pubkey::new_unique();
    let first = Pubkey::new_unique();
    let second = Pubkey::new_unique();

    create_test_mint(&cheats, &mint).unwrap();
    cheats
        .fund_token_many(&[&first, &second], &mint, 999, None)
        .unwrap();

    assert_eq!(read_token_amount(&cheats, &first, &mint), 999);
    assert_eq!(read_token_amount(&cheats, &second, &mint), 999);
}

#[tokio::test(flavor = "multi_thread")]
async fn time_travel_to_slot_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let client = rpc_client(&cheats);
    let before = client.get_epoch_info().unwrap();
    let target_slot = before.absolute_slot + 100;

    let after = cheats.time_travel_to_slot(target_slot).unwrap();
    assert!(after.absolute_slot >= target_slot);
}

#[tokio::test(flavor = "multi_thread")]
async fn time_travel_to_epoch_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let client = rpc_client(&cheats);
    let before = client.get_epoch_info().unwrap();
    let target_epoch = before.epoch + 1;

    let after = cheats.time_travel_to_epoch(target_epoch).unwrap();
    assert!(after.epoch >= target_epoch);
}

#[tokio::test(flavor = "multi_thread")]
async fn time_travel_to_timestamp_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let client = rpc_client(&cheats);
    let before = client.get_epoch_info().unwrap();
    let target_timestamp_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
        + 5_000;

    let after = cheats
        .time_travel_to_timestamp(target_timestamp_ms)
        .unwrap();
    assert!(after.absolute_slot > before.absolute_slot);
}

#[tokio::test(flavor = "multi_thread")]
async fn reset_account_builder_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let client = rpc_client(&cheats);
    let address = Pubkey::new_unique();
    let owner = Pubkey::new_unique();

    cheats
        .set_account(&address, 77_777, &[1, 2, 3], &owner)
        .unwrap();
    assert_eq!(client.get_balance(&address).unwrap(), 77_777);

    cheats
        .execute(ResetAccount::new(address).include_owned_accounts(false))
        .unwrap();

    assert_eq!(client.get_balance(&address).unwrap(), 0);
    assert!(client.get_account(&address).is_err());
}

#[tokio::test(flavor = "multi_thread")]
async fn stream_account_builder_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let client = rpc_client(&cheats);
    let address = Pubkey::new_unique();

    cheats
        .execute(StreamAccount::new(address).include_owned_accounts(true))
        .unwrap();

    let response = client
        .send::<serde_json::Value>(
            solana_client::rpc_request::RpcRequest::Custom {
                method: "surfnet_getStreamedAccounts",
            },
            serde_json::json!([]),
        )
        .unwrap();

    let accounts = response
        .get("value")
        .and_then(|value| value.get("accounts"))
        .and_then(|value| value.as_array())
        .cloned()
        .unwrap_or_default();

    assert!(accounts.iter().any(|account| {
        account.get("pubkey").and_then(|value| value.as_str()) == Some(&address.to_string())
            && account
                .get("includeOwnedAccounts")
                .and_then(|value| value.as_bool())
                == Some(true)
    }));
}

#[tokio::test(flavor = "multi_thread")]
async fn deploy_program_builder_executes_against_surfnet() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let client = rpc_client(&cheats);
    let program_id = Pubkey::new_unique();
    let temp = tempdir().unwrap();
    let idl_path = temp.path().join("program.json");

    fs::write(
        &idl_path,
        serde_json::to_vec(&sample_idl(&program_id)).unwrap(),
    )
    .unwrap();

    cheats
        .deploy(
            DeployProgram::new(program_id)
                .so_bytes(vec![1, 2, 3, 4, 5, 6])
                .idl_path(&idl_path),
        )
        .unwrap();

    let account = client.get_account(&program_id).unwrap();
    assert!(account.executable);
}

#[tokio::test(flavor = "multi_thread")]
async fn deploy_program_discovers_workspace_artifacts() {
    let _guard = test_lock();
    let surfnet = Surfnet::start().await.unwrap();
    let cheats = surfnet.cheatcodes();
    let client = rpc_client(&cheats);
    let temp = tempdir().unwrap();
    let previous_dir = std::env::current_dir().unwrap();
    let deploy_dir = temp.path().join("target/deploy");
    let idl_dir = temp.path().join("target/idl");
    let keypair = crate::Keypair::new();
    let program_name = "fixture_program";

    fs::create_dir_all(&deploy_dir).unwrap();
    fs::create_dir_all(&idl_dir).unwrap();
    fs::write(
        deploy_dir.join(format!("{program_name}.so")),
        vec![9, 8, 7, 6],
    )
    .unwrap();
    fs::write(
        deploy_dir.join(format!("{program_name}-keypair.json")),
        serde_json::to_vec(&keypair.to_bytes().to_vec()).unwrap(),
    )
    .unwrap();
    fs::write(
        idl_dir.join(format!("{program_name}.json")),
        serde_json::to_vec(&sample_idl(&keypair.pubkey())).unwrap(),
    )
    .unwrap();

    std::env::set_current_dir(temp.path()).unwrap();
    let result = cheats.deploy_program(program_name);
    std::env::set_current_dir(previous_dir).unwrap();
    result.unwrap();

    let account = client.get_account(&keypair.pubkey()).unwrap();
    assert!(account.executable);
}

#[test]
fn get_ata_matches_associated_token_derivation() {
    let owner = Pubkey::new_unique();
    let mint = Pubkey::new_unique();
    let cheats = Cheatcodes::new("http://127.0.0.1:8899");

    let derived = cheats.get_ata(&owner, &mint, None);
    let expected =
        get_associated_token_address_with_program_id(&owner, &mint, &spl_token_program_id());

    assert_eq!(derived, expected);
}

#[test]
fn resolve_target_dir_walks_up_to_project_root_target() {
    let temp = tempdir().unwrap();
    let previous_dir = std::env::current_dir().unwrap();
    let nested_crate_dir = temp.path().join("programs/simple-pda/tests");
    let deploy_dir = temp.path().join("target/deploy");
    let keypair = crate::Keypair::new();
    let program_name = "simple_pda";

    fs::create_dir_all(&nested_crate_dir).unwrap();
    fs::create_dir_all(&deploy_dir).unwrap();
    fs::write(deploy_dir.join(format!("{program_name}.so")), vec![1, 2, 3]).unwrap();
    fs::write(
        deploy_dir.join(format!("{program_name}-keypair.json")),
        serde_json::to_vec(&keypair.to_bytes().to_vec()).unwrap(),
    )
    .unwrap();

    std::env::set_current_dir(&nested_crate_dir).unwrap();
    let result = resolve_target_dir(program_name);
    std::env::set_current_dir(previous_dir).unwrap();

    assert_eq!(
        result.unwrap().canonicalize().unwrap(),
        temp.path().join("target").canonicalize().unwrap()
    );
}
