//! Integration tests for `infer()` end-to-end through real HTTP to MockServer.
//!
//! Covers the middle layer between:
//! - `test_infer.rs` (unit tests via hand-crafted mock channel, bypass HTTP)
//! - `test_channel_error_classification.rs` (golden tests at SSE protocol layer)
//!
//! These tests verify that `OpenAiChannel` + `infer()` together correctly:
//! - Consume SSE from MockServer and parse Parseable response
//! - Classify HTTP errors (402/429/5xx) into `InferErrorCode`
//! - Apply retry logic per INFER_MAX_RETRIES
//! - Surface quota errors without retry

mod common;

use common::mock_llm_server::{MockLlmServer, Script};
use mad_hatter::{infer, InferErrorCode, OpenAiChannel, Parseable, Promptable};

// ============================================================
// Test types
// ============================================================

#[derive(Promptable)]
#[prompt("Test request")]
struct TestRequest {
    #[prompt("Message")]
    message: String,
}

#[derive(Parseable, PartialEq, Debug)]
enum TestAction {
    #[prompt("Reply to user")]
    #[allow(dead_code)]
    Reply {
        content: String,
    },
    #[prompt("Do nothing")]
    #[allow(dead_code)]
    Idle,
}

// ============================================================
// Helper: extract token from a rendered schema to build valid output
// ============================================================

/// Build a valid LLM output for TestAction::Idle given the action token.
/// Format: `TestAction-{token}\nIdle\nend-marker-{token}`
fn build_idle_output(token: &str) -> String {
    format!("TestAction-{tok}\nIdle\nend-marker-{tok}\n", tok = token)
}

/// Build a valid LLM output for TestAction::Reply with the given content.
fn build_reply_output(token: &str, content: &str) -> String {
    format!(
        "TestAction-{tok}\nReply\ncontent-{tok}\n{c}\nend-marker-{tok}\n",
        tok = token,
        c = content
    )
}

/// Render the TestAction schema and extract the token (the randomly-generated
/// suffix after "TestAction-"). This mirrors what the infer engine does to
/// synchronize output tokens with the schema it sent to the LLM.
///
/// Since infer() generates a fresh token per call internally, we can't predict
/// it from outside. Instead, we use MockServer's script-inspection to see what
/// prompt the engine sent, extract the token, then build a reply that matches.
///
/// For the simpler tests here, we use a trick: script `SseContent` with a
/// *regex-extractable* answer, and let the mock respond after the request is
/// captured. But that requires two-phase mock behavior which we don't have.
///
/// Simpler trick: prime the mock with a Script::DeferredScript that reads the
/// first request's body, extracts the token from the schema embedded in the
/// prompt, then responds. But that's also beyond current mock capability.
///
/// **Pragmatic approach**: the token in the prompt is unique (regenerated per
/// infer call). We inspect `mock.requests()` AFTER the infer returns error or
/// success to understand what happened. For happy-path tests, we need to
/// either:
/// 1. Pre-capture the token by first firing a dummy request, OR
/// 2. Use a response format that doesn't require token matching (impossible
///    since end marker is required).
///
/// Current state: we use a deferred-response mock pattern that inspects the
/// request on the fly. For now, we test error paths (which don't need
/// token-correct responses) and add happy-path once mock supports it.
fn _unused_token_trick() {}

// ============================================================
// Error classification tests (don't need valid LLM output — response is an error)
// ============================================================

#[tokio::test]
async fn test_http_402_propagates_as_quota_no_retry() {
    let mock = MockLlmServer::start().await;
    // Only push ONE script — if infer retries, subsequent requests fail with
    // "no script queued" which is a different error. 402 should NOT retry.
    mock.push_script(Script::quota_exceeded()).await;

    let channel = OpenAiChannel::new(mock.chat_url(), "test-model", "test-key");
    let req = TestRequest {
        message: "hello".to_string(),
    };

    let result = infer::<_, TestAction>(&channel, &req, false).await;

    assert!(result.is_err(), "expected error, got: {:?}", result);
    let err = result.unwrap_err();
    assert!(
        matches!(err.code, InferErrorCode::HttpStatus(402)),
        "expected HttpStatus(402), got: {:?}",
        err.code
    );
    assert!(err.code.is_quota(), "expected is_quota=true");
    assert!(!err.code.is_retryable(), "402 must not be retryable");

    // Verify only 1 request sent (no retry)
    assert_eq!(
        mock.request_count().await,
        1,
        "402 should not trigger retry"
    );
}

#[tokio::test]
async fn test_http_429_retries_then_surfaces_error() {
    let mock = MockLlmServer::start().await;
    // INFER_MAX_RETRIES=2, so 3 total attempts all failing with 429
    mock.push_script(Script::rate_limited()).await;
    mock.push_script(Script::rate_limited()).await;
    mock.push_script(Script::rate_limited()).await;

    let channel = OpenAiChannel::new(mock.chat_url(), "test-model", "test-key");
    let req = TestRequest {
        message: "hello".to_string(),
    };

    let result = infer::<_, TestAction>(&channel, &req, false).await;

    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(
        matches!(err.code, InferErrorCode::HttpStatus(429)),
        "expected HttpStatus(429), got: {:?}",
        err.code
    );
    assert!(!err.code.is_quota());
    assert!(err.code.is_retryable(), "429 should be retryable");

    // Should have retried — at least 2 requests. Exact count depends on
    // retry schedule + backoff timing. We assert >= 2 (retried at least once).
    let count = mock.request_count().await;
    assert!(count >= 2, "expected retry, got {} requests", count);
}

#[tokio::test]
async fn test_http_500_classified_as_http_status() {
    let mock = MockLlmServer::start().await;
    mock.push_script(Script::server_error()).await;
    mock.push_script(Script::server_error()).await;
    mock.push_script(Script::server_error()).await;

    let channel = OpenAiChannel::new(mock.chat_url(), "test-model", "test-key");
    let req = TestRequest {
        message: "hello".to_string(),
    };

    let result = infer::<_, TestAction>(&channel, &req, false).await;

    assert!(result.is_err());
    let err = result.unwrap_err();
    assert!(
        matches!(err.code, InferErrorCode::HttpStatus(500)),
        "expected HttpStatus(500), got: {:?}",
        err.code
    );
    assert!(err.code.is_channel());
    assert!(err.code.is_retryable());
}

#[tokio::test]
async fn test_sse_zero_chunks_surfaces_error() {
    let mock = MockLlmServer::start().await;
    // SSE stream that says [DONE] without any content chunks.
    // This is a transport-level error from the engine's perspective — maps to
    // StartStreamFailed or similar channel-level error. Push enough for retries.
    mock.push_script(Script::SseDoneNoChunks).await;
    mock.push_script(Script::SseDoneNoChunks).await;
    mock.push_script(Script::SseDoneNoChunks).await;

    let channel = OpenAiChannel::new(mock.chat_url(), "test-model", "test-key");
    let req = TestRequest {
        message: "hello".to_string(),
    };

    let result = infer::<_, TestAction>(&channel, &req, false).await;

    assert!(result.is_err(), "expected error for zero-chunks SSE");
    // Accept either channel-level classification (StartStreamFailed) or
    // output-level (FormatViolation/Truncated/ParseError) depending on how the
    // engine surfaces it. The key contract: it errors out, doesn't hang.
    let err = result.unwrap_err();
    eprintln!("zero-chunks surfaced as: {:?}", err.code);
    // Don't assert specific variant here — just that some error surfaced.
}

// ============================================================
// Happy path tests — DEFERRED
//
// Need mock capability to inspect the incoming request's embedded token and
// respond with a token-matched valid LLM output. Current MockServer doesn't
// support request-responsive scripting. Will be added in a follow-up that
// extends MockServer with `Script::Reflective(Box<dyn Fn(&CapturedRequest) -> Script>)`.
//
// For now, Parseable output parsing is well-covered by test_infer.rs (via
// hand-crafted mock channel), and HTTP layer by test_channel_error_classification.rs.
// ============================================================

#[allow(dead_code)]
fn _placeholder_happy_path() {
    let _ = build_idle_output("abc123");
    let _ = build_reply_output("abc123", "hello world");
}