use super::{
    MAX_ITEMS_PER_BATCH, OpenAIExtractorError, append_prompt_context, available_batch_input_tokens,
    build_batches, is_oversized_rate_limit_error, max_output_tokens_for_batch, parse_retry_after,
    response_format_description, response_format_name, strict_json_schema, truncate_to_tokens,
};
use crate::Info;
use schemars::JsonSchema;
use serde::de::Error as _;
use serde::{Deserialize, Serialize};
use validator::Validate;

fn json_error(message: &str) -> OpenAIExtractorError {
    OpenAIExtractorError::SerdeJsonError(serde_json::Error::custom(message.to_string()))
}

#[test]
fn retry_after_parses_minutes_and_seconds() {
    let error = json_error("Please retry after 1m2.5s");

    assert_eq!(parse_retry_after(&error), 62.5);
}

#[test]
fn retry_after_defaults_when_duration_missing() {
    let error = json_error("rate limit exceeded");

    assert_eq!(parse_retry_after(&error), 60.0);
}

#[test]
fn oversized_rate_limit_detection_matches_tpm_request_too_large() {
    let error = json_error(
        "Request too large for gpt-5-nano on tokens per min (TPM): Limit 200000, Requested 217012.",
    );

    assert!(is_oversized_rate_limit_error(&error));
}

#[test]
fn truncate_to_tokens_respects_char_boundaries() {
    let text = "é".repeat(20);
    let truncated = truncate_to_tokens(&text, 3);

    assert!(truncated.ends_with("... [truncated]"));
    assert!(truncated.is_char_boundary(truncated.len()));
}

#[test]
fn strict_json_schema_sets_additional_properties_false_recursively() {
    let schema = serde_json::json!({
        "type": "object",
        "properties": {
            "items": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "nested": {
                            "type": "object",
                            "properties": {
                                "name": { "type": "string" }
                            }
                        }
                    }
                }
            }
        }
    });

    let strict = strict_json_schema(schema);

    assert_eq!(strict["additionalProperties"], false);
    assert_eq!(strict["required"], serde_json::json!(["items"]));
    assert_eq!(
        strict["properties"]["items"]["items"]["additionalProperties"],
        false
    );
    assert_eq!(
        strict["properties"]["items"]["items"]["required"],
        serde_json::json!(["nested"])
    );
    assert_eq!(
        strict["properties"]["items"]["items"]["properties"]["nested"]["additionalProperties"],
        false
    );
    assert_eq!(
        strict["properties"]["items"]["items"]["properties"]["nested"]["required"],
        serde_json::json!(["name"])
    );
}

#[test]
fn response_format_name_uses_sanitized_schema_title() {
    let schema = serde_json::json!({
        "title": "Company Info v1!"
    });

    assert_eq!(response_format_name(&schema), "CompanyInfov1");
}

#[test]
fn response_format_description_uses_schema_description() {
    let schema = serde_json::json!({
        "description": "Structured company extraction output."
    });

    assert_eq!(
        response_format_description(&schema),
        "Structured company extraction output."
    );
}

#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Validate)]
struct PromptInfo {
    value: String,
}

impl Info for PromptInfo {
    fn json_schema() -> serde_json::Value {
        serde_json::json!({})
    }

    fn extraction_instructions() -> Option<&'static str> {
        Some("Dates must be yyyy-MM-dd.")
    }

    fn continuity_instructions(_items: &[Self]) -> Option<String> {
        Some("Use NEC for the Birmingham venue.".to_string())
    }
}

#[test]
fn append_prompt_context_adds_rules_and_memory() {
    let mut prompt = "Base prompt".to_string();
    append_prompt_context::<PromptInfo>(&mut prompt, &[]);

    assert!(prompt.contains("Base prompt"));
    assert!(prompt.contains("Additional extraction rules:"));
    assert!(prompt.contains("Dates must be yyyy-MM-dd."));
    assert!(prompt.contains("Canonical outputs already accepted in this run:"));
    assert!(prompt.contains("Use NEC for the Birmingham venue."));
}

#[test]
fn build_batches_respects_item_count_cap() {
    let item_tokens = vec![1_000; MAX_ITEMS_PER_BATCH + 3];
    let batches = build_batches(&item_tokens);

    assert_eq!(batches.len(), 2);
    assert_eq!(batches[0].len(), MAX_ITEMS_PER_BATCH);
    assert_eq!(batches[1].len(), 3);
}

#[test]
fn build_batches_respects_token_budget() {
    let item_size = available_batch_input_tokens() / 10;
    let item_tokens = vec![item_size; 11];
    let batches = build_batches(&item_tokens);

    assert_eq!(batches, vec![(0..10).collect::<Vec<_>>(), vec![10]]);
}

#[test]
fn single_item_batches_get_a_larger_output_budget() {
    assert_eq!(max_output_tokens_for_batch(1), 8_000);
    assert_eq!(max_output_tokens_for_batch(2), 4_000);
}
