use std::{env, path::PathBuf, str::FromStr, time::Instant};

use crate::init_tracing;
use alloy::transports::http::reqwest::Url as RpcUrl;
use alloy_primitives::{Address, B256, U256};
use alloy_provider::RootProvider;
use anyhow::{Context, Result, ensure};
use rpc::{
    SubscriptionSource,
    client::{Client, ClientConfig, connect_provider_with_timeout},
};
use test_context::AsyncTestContext;
use tracing::info;

use super::helpers::{
    align_l1_time_past_l2_head, create_snapshot, ensure_preconf_whitelist_active,
    get_proposal_hash, reset_head_l1_origin, reset_to_base_block, revert_snapshot,
};

/// Environment configuration required to exercise Shasta fork integration tests against
/// the Docker harness started by `tests/entrypoint.sh`.
/// Holds resolved endpoints, credentials, and clients needed to drive Shasta integration flows.
pub struct ShastaEnv {
    pub inbox_address: Address,
    pub l2_suggested_fee_recipient: Address,
    pub l1_proposer_private_key: B256,
    pub taiko_anchor_address: Address,
    pub client_config: ClientConfig,
    cleanup_provider: RootProvider,
    snapshot_id: String,
}

impl ShastaEnv {
    fn load_l1_source() -> Result<SubscriptionSource> {
        let l1_ws = env::var("HARNESS_L1_WS").context("HARNESS_L1_WS env var is required")?;
        let l1_ws_url = RpcUrl::parse(l1_ws.as_str()).context("invalid HARNESS_L1_WS endpoint")?;
        Ok(SubscriptionSource::Ws(l1_ws_url))
    }

    fn load_l2_secondary_endpoints() -> Result<(RpcUrl, RpcUrl)> {
        let l2_ws_1 = env::var("L2_WS_1").context("L2_WS_1 env var is required")?;
        let l2_auth_1 = env::var("L2_AUTH_1").context("L2_AUTH_1 env var is required")?;

        let l2_ws_1_url = RpcUrl::parse(l2_ws_1.as_str()).context("invalid L2_WS_1 endpoint")?;
        let l2_auth_1_url =
            RpcUrl::parse(l2_auth_1.as_str()).context("invalid L2_AUTH_1 endpoint")?;

        Ok((l2_ws_1_url, l2_auth_1_url))
    }

    /// Resolves required environment variables and builds a default RPC client bundle.
    pub async fn load_from_env() -> Result<Self> {
        let started = Instant::now();

        // Initialize tracing for the test harness.
        init_tracing("info");

        // Read all required endpoints, secrets, and addresses from the harness environment.
        let l1_source = Self::load_l1_source()?;
        let l2_ws_0 = env::var("L2_WS_0").context("L2_WS_0 env var is required")?;
        let l2_auth_0 = env::var("L2_AUTH_0").context("L2_AUTH_0 env var is required")?;
        let jwt_secret = env::var("JWT_SECRET").context("JWT_SECRET env var is required")?;
        let inbox = env::var("SHASTA_INBOX").context("SHASTA_INBOX env var is required")?;
        let fee_recipient = env::var("L2_SUGGESTED_FEE_RECIPIENT")
            .context("L2_SUGGESTED_FEE_RECIPIENT env var is required")?;
        let proposer_key = env::var("L1_PROPOSER_PRIVATE_KEY")
            .context("L1_PROPOSER_PRIVATE_KEY env var is required")?;
        let anchor = env::var("TAIKO_ANCHOR").context("TAIKO_ANCHOR env var is required")?;

        // Parse raw strings into URLs, paths, and addresses.
        let l2_ws_0_url = RpcUrl::parse(l2_ws_0.as_str()).context("invalid L2_WS_0 endpoint")?;
        let l2_auth_0_url =
            RpcUrl::parse(l2_auth_0.as_str()).context("invalid L2_AUTH_0 endpoint")?;
        let jwt_secret_path = PathBuf::from(jwt_secret);
        let inbox_address = Address::from_str(inbox.as_str()).context("invalid SHASTA_INBOX")?;
        let l2_suggested_fee_recipient = Address::from_str(fee_recipient.as_str())
            .context("invalid L2_SUGGESTED_FEE_RECIPIENT")?;
        let l1_proposer_private_key =
            proposer_key.parse().context("invalid L1_PROPOSER_PRIVATE_KEY hex value")?;
        let taiko_anchor_address =
            Address::from_str(anchor.as_str()).context("invalid TAIKO_ANCHOR address")?;
        let (l2_ws_1, l2_auth_1) = Self::load_l2_secondary_endpoints()?;

        // Build shared RPC client bundle and a dedicated provider for snapshots.
        let client_config = ClientConfig {
            l1_provider_source: l1_source.clone(),
            l2_provider_url: l2_ws_0_url.clone(),
            l2_auth_provider_url: l2_auth_0_url.clone(),
            jwt_secret: jwt_secret_path.clone(),
            inbox_address,
        };
        let client = Client::new(client_config.clone()).await?;

        // Teardown's `evm_revert` never runs when a test PROCESS is killed (hang timeout,
        // OOM, Ctrl-C), which would bake that test's proposals into the shared L1 for the
        // rest of the run and wedge every later test on unfetchable blobs. Fail fast with
        // a restart hint instead of inheriting the orphaned state.
        let leftover = get_proposal_hash(&client, U256::ONE).await?;
        ensure!(
            leftover == B256::ZERO,
            "shared L1 already contains proposal 1 (hash {leftover}); an earlier test \
             process died without reverting its snapshot — recreate the docker env \
             (rerun `just test`)"
        );

        // Reset both L2 nodes to a known base block before tests run.
        reset_to_base_block(&client).await?;
        reset_head_l1_origin(&client).await?;

        let secondary_config = ClientConfig {
            l1_provider_source: l1_source.clone(),
            l2_provider_url: l2_ws_1.clone(),
            l2_auth_provider_url: l2_auth_1.clone(),
            jwt_secret: jwt_secret_path.clone(),
            inbox_address,
        };
        let secondary_client = Client::new(secondary_config).await?;
        reset_to_base_block(&secondary_client).await?;
        reset_head_l1_origin(&secondary_client).await?;

        // Restore the real-chain invariant that L1 time is ahead of every persisted L2
        // block BEFORE snapshotting, so the ratchet survives this test's revert.
        align_l1_time_past_l2_head(&client).await?;

        // Take a fresh snapshot and activate preconf whitelist before tests run.
        let cleanup_provider = connect_provider_with_timeout(l1_source.url().clone()).await?;
        let snapshot_id = create_snapshot(&cleanup_provider).await?;
        ensure_preconf_whitelist_active(&client).await?;

        info!(elapsed_ms = started.elapsed().as_millis(), "loaded ShastaEnv");
        Ok(Self {
            inbox_address,
            l2_suggested_fee_recipient,
            l1_proposer_private_key,
            taiko_anchor_address,
            client_config,
            cleanup_provider,
            snapshot_id,
        })
    }

    /// Explicit async teardown to revert the L1 snapshot.
    pub async fn shutdown(self) -> Result<()> {
        revert_snapshot(&self.cleanup_provider, &self.snapshot_id).await
    }
}

impl AsyncTestContext for ShastaEnv {
    /// Setup the ShastaEnv before each test.
    async fn setup() -> Self {
        ShastaEnv::load_from_env()
            .await
            .unwrap_or_else(|err| panic!("failed to load ShastaEnv: {err:#}"))
    }

    /// Teardown the ShastaEnv after each test.
    async fn teardown(self) {
        self.shutdown().await.unwrap_or_else(|err| panic!("ShastaEnv teardown failed: {err:?}"));
    }
}

#[cfg(test)]
mod tests {
    use super::{ShastaEnv, SubscriptionSource};
    use std::{
        env,
        sync::{LazyLock, Mutex},
    };

    static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

    struct EnvGuard {
        key: &'static str,
        previous: Option<String>,
    }

    impl EnvGuard {
        fn set(key: &'static str, value: &str) -> Self {
            let previous = env::var(key).ok();
            // SAFETY: ENV_LOCK serializes all process-environment mutations in this module, and
            // these tests do not read or write the guarded variables outside the lock scope.
            unsafe { env::set_var(key, value) };
            Self { key, previous }
        }

        fn unset(key: &'static str) -> Self {
            let previous = env::var(key).ok();
            // SAFETY: ENV_LOCK serializes all process-environment mutations in this module, and
            // these tests do not read or write the guarded variables outside the lock scope.
            unsafe { env::remove_var(key) };
            Self { key, previous }
        }
    }

    impl Drop for EnvGuard {
        fn drop(&mut self) {
            // SAFETY: ENV_LOCK serializes all process-environment mutations in this module, and
            // these tests do not read or write the guarded variables outside the lock scope.
            unsafe {
                match &self.previous {
                    Some(value) => env::set_var(self.key, value),
                    None => env::remove_var(self.key),
                }
            }
        }
    }

    #[test]
    fn secondary_l2_endpoints_accept_ws_only() {
        let _lock = ENV_LOCK.lock().expect("env lock poisoned");
        let _ws = EnvGuard::set("L2_WS_1", "ws://localhost:38546");
        let _auth = EnvGuard::set("L2_AUTH_1", "http://localhost:38551");

        let result = ShastaEnv::load_l2_secondary_endpoints();

        assert!(result.is_ok());
        let (l2_ws_1, l2_auth_1) = result.unwrap();
        assert_eq!(l2_ws_1.as_str(), "ws://localhost:38546/");
        assert_eq!(l2_auth_1.as_str(), "http://localhost:38551/");
    }

    #[test]
    fn secondary_l2_endpoints_fail_when_unset() {
        let _lock = ENV_LOCK.lock().expect("env lock poisoned");
        let _ws = EnvGuard::unset("L2_WS_1");
        let _auth = EnvGuard::unset("L2_AUTH_1");

        let result = ShastaEnv::load_l2_secondary_endpoints();

        assert!(result.is_err());
    }

    #[test]
    fn l1_source_uses_harness_ws() {
        let _lock = ENV_LOCK.lock().expect("env lock poisoned");
        let _ws = EnvGuard::set("HARNESS_L1_WS", "ws://localhost:18545");

        let result = ShastaEnv::load_l1_source();

        assert!(result.is_ok());
        let source = result.unwrap();
        assert!(matches!(source, SubscriptionSource::Ws(_)));
        assert_eq!(source.url().as_str(), "ws://localhost:18545/");
    }

    #[test]
    fn l1_source_rejects_missing_ws() {
        let _lock = ENV_LOCK.lock().expect("env lock poisoned");
        let _ws = EnvGuard::unset("HARNESS_L1_WS");

        let result = ShastaEnv::load_l1_source();

        assert!(result.is_err());
    }

    #[test]
    fn l1_http_source_can_be_constructed_from_harness_env() {
        let _lock = ENV_LOCK.lock().expect("env lock poisoned");
        let _http = EnvGuard::set("HARNESS_L1_HTTP", "http://localhost:18545");

        let url = env::var("HARNESS_L1_HTTP").unwrap();
        let source: SubscriptionSource = url.as_str().try_into().unwrap();

        assert!(matches!(source, SubscriptionSource::Http(_)));
        assert_eq!(source.url().as_str(), "http://localhost:18545/");
    }
}
