//! End-to-End Tests for `tli train status` Command
//!
//! Tests training job status queries

use fxt::proto::ml_training::{
    ml_training_service_server::MlTrainingServiceServer, TrainingStatus,
    ml_training_service_client::MlTrainingServiceClient, GetTrainingJobDetailsRequest,
};
use tonic::transport::{Server, Channel};
use std::net::SocketAddr;
use tokio::time::Duration;

use super::mock_ml_training_service::{MockMlTrainingService, MockTrainingState};
use super::test_fixtures;

/// Helper to start mock gRPC server
async fn start_mock_server(state: MockTrainingState) -> SocketAddr {
    let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
    let service = MockMlTrainingService::new(state);

    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    let local_addr = listener.local_addr().unwrap();

    tokio::spawn(async move {
        Server::builder()
            .add_service(MlTrainingServiceServer::new(service))
            .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
            .await
            .unwrap();
    });

    tokio::time::sleep(Duration::from_millis(100)).await;
    local_addr
}

/// TEST 1: Query by job ID
#[tokio::test]
async fn test_query_by_job_id() {
    let state = MockTrainingState::default();

    let job = test_fixtures::create_test_job(
        "status_test_1",
        "TFT",
        TrainingStatus::Running,
    );
    state.add_job(job);

    let addr = start_mock_server(state.clone()).await;

    let channel = Channel::from_shared(format!("http://{}", addr))
        .unwrap()
        .connect()
        .await
        .unwrap();

    let mut client = MlTrainingServiceClient::new(channel);

    let request = GetTrainingJobDetailsRequest {
        job_id: "status_test_1".to_string(),
    };

    let response = client.get_training_job_details(request).await;
    assert!(response.is_ok(), "Should successfully query job by ID");
}

/// TEST 2: Query by batch ID
#[tokio::test]
async fn test_query_by_batch_id() {
    let state = MockTrainingState::default();

    // Create batch jobs
    for i in 0..4 {
        let job = test_fixtures::create_test_job(
            &format!("batch_20251022_140000_{}", i),
            "TFT",
            TrainingStatus::Running,
        );
        state.add_job(job);
    }

    let _addr = start_mock_server(state.clone()).await;

    let jobs = state.list_jobs();
    let batch_jobs: Vec<_> = jobs
        .iter()
        .filter(|j| j.job_id.contains("batch_20251022_140000"))
        .collect();

    assert_eq!(batch_jobs.len(), 4, "Should find all batch jobs");
}

/// TEST 3: Query by asset symbol
#[tokio::test]
async fn test_query_by_asset() {
    let state = MockTrainingState::default();

    let job = test_fixtures::create_test_job(
        "train_tft_es_fut_20251022",
        "TFT",
        TrainingStatus::Completed,
    );
    state.add_job(job);

    let _addr = start_mock_server(state.clone()).await;

    let jobs = state.list_jobs();
    let es_jobs: Vec<_> = jobs
        .iter()
        .filter(|j| j.job_id.contains("es_fut"))
        .collect();

    assert_eq!(es_jobs.len(), 1);
}

/// TEST 4: Non-existent job → 404 error
#[tokio::test]
async fn test_nonexistent_job_404() {
    let state = MockTrainingState::default();
    let addr = start_mock_server(state).await;

    let channel = Channel::from_shared(format!("http://{}", addr))
        .unwrap()
        .connect()
        .await
        .unwrap();

    let mut client = MlTrainingServiceClient::new(channel);

    let request = GetTrainingJobDetailsRequest {
        job_id: "nonexistent_job_123".to_string(),
    };

    let response = client.get_training_job_details(request).await;
    assert!(response.is_err(), "Should return 404 for non-existent job");
    assert!(response.unwrap_err().message().contains("not found"));
}

/// TEST 5: Status output formatting validation
#[tokio::test]
async fn test_status_output_formatting() {
    let state = MockTrainingState::default();

    let job = test_fixtures::create_test_job_with_times(
        "format_test",
        "DQN",
        TrainingStatus::Completed,
        1729602000,
        1729602060,
        1729602464,
    );
    state.add_job(job.clone());

    let _addr = start_mock_server(state).await;

    // Verify job has all required fields for formatting
    assert!(!job.job_id.is_empty());
    assert!(!job.model_type.is_empty());
    assert!(job.created_at > 0);
    assert!(job.started_at > 0);
    assert!(job.completed_at > 0);
    assert_eq!(job.status, TrainingStatus::Completed as i32);
}

/// TEST 6: Multiple status queries (batch)
#[tokio::test]
async fn test_multiple_status_queries() {
    let state = MockTrainingState::default();

    let jobs = test_fixtures::create_test_job_batch();
    for job in &jobs {
        state.add_job(job.clone());
    }

    let addr = start_mock_server(state).await;

    let channel = Channel::from_shared(format!("http://{}", addr))
        .unwrap()
        .connect()
        .await
        .unwrap();

    let mut client = MlTrainingServiceClient::new(channel);

    // Query each job
    for job in &jobs {
        let request = GetTrainingJobDetailsRequest {
            job_id: job.job_id.clone(),
        };

        let response = client.get_training_job_details(request).await;
        assert!(response.is_ok(), "Should query job {}", job.job_id);
    }
}

/// TEST 7: Status query with metrics
#[tokio::test]
async fn test_status_with_metrics() {
    let state = MockTrainingState::default();

    let job = test_fixtures::create_test_job(
        "metrics_test",
        "PPO",
        TrainingStatus::Running,
    );
    state.add_job(job.clone());

    let _addr = start_mock_server(state).await;

    // Verify metrics fields exist
    assert!(job.final_loss >= 0.0);
    assert!(job.best_validation_score >= 0.0);
}

/// TEST 8: Pending job status
#[tokio::test]
async fn test_pending_job_status() {
    let state = MockTrainingState::default();

    let job = test_fixtures::create_test_job(
        "pending_test",
        "MAMBA_2",
        TrainingStatus::Pending,
    );
    state.add_job(job.clone());

    let _addr = start_mock_server(state).await;

    assert_eq!(job.status, TrainingStatus::Pending as i32);
    assert_eq!(job.started_at, 0);
    assert_eq!(job.completed_at, 0);
}

/// TEST 9: Failed job status with error
#[tokio::test]
async fn test_failed_job_status() {
    let state = MockTrainingState::default();

    let job = test_fixtures::create_test_job(
        "failed_test",
        "TFT",
        TrainingStatus::Failed,
    );
    state.add_job(job.clone());

    let _addr = start_mock_server(state).await;

    assert_eq!(job.status, TrainingStatus::Failed as i32);
}

/// TEST 10: Stopped job status
#[tokio::test]
async fn test_stopped_job_status() {
    let state = MockTrainingState::default();

    let job = test_fixtures::create_test_job(
        "stopped_test",
        "DQN",
        TrainingStatus::Stopped,
    );
    state.add_job(job.clone());

    let _addr = start_mock_server(state).await;

    assert_eq!(job.status, TrainingStatus::Stopped as i32);
}
