//! Tests for FilterPlugin trait, FilterOutput, and FilterError.

use bytes::Bytes;
use cc_lb_plugin_api::{
    FilterError, FilterOutput, FilterPlugin, Principal, PrincipalKind, RequestContext,
    TerminalStrategy, UpstreamCandidate, UpstreamKind,
};
use http::Method;
use uuid::Uuid;

#[test]
fn filter_output_equality() {
    let output1 = FilterOutput {
        kept_upstream_ids: vec![Uuid::new_v4()],
        reason: "allowed".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    let output2 = FilterOutput {
        kept_upstream_ids: output1.kept_upstream_ids.clone(),
        reason: "allowed".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    assert_eq!(output1, output2);
}

#[test]
fn filter_output_inequality_different_ids() {
    let id1 = Uuid::new_v4();
    let id2 = Uuid::new_v4();
    let output1 = FilterOutput {
        kept_upstream_ids: vec![id1],
        reason: "allowed".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    let output2 = FilterOutput {
        kept_upstream_ids: vec![id2],
        reason: "allowed".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    assert_ne!(output1, output2);
}

#[test]
fn filter_output_inequality_different_reason() {
    let id = Uuid::new_v4();
    let output1 = FilterOutput {
        kept_upstream_ids: vec![id],
        reason: "allowed".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    let output2 = FilterOutput {
        kept_upstream_ids: vec![id],
        reason: "rejected".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    assert_ne!(output1, output2);
}

#[test]
fn filter_output_debug_formatting() {
    let id = Uuid::new_v4();
    let output = FilterOutput {
        kept_upstream_ids: vec![id],
        reason: "test reason".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    let debug_str = format!("{:?}", output);
    assert!(debug_str.contains("FilterOutput"));
    assert!(debug_str.contains("test reason"));
}

#[test]
fn filter_output_clone() {
    let id = Uuid::new_v4();
    let output = FilterOutput {
        kept_upstream_ids: vec![id],
        reason: "original".to_string(),
        per_candidate_reasons: Vec::new(),
    };
    let cloned = output.clone();
    assert_eq!(output, cloned);
}

#[test]
fn filter_error_runtime_display() {
    let err = FilterError::Runtime {
        reason: "custom filter failed".to_string(),
    };
    let display_str = format!("{}", err);
    assert!(display_str.contains("filter runtime error"));
    assert!(display_str.contains("custom filter failed"));
}

#[test]
fn filter_error_trap_display() {
    let err = FilterError::Trap {
        reason: "plugin crashed".to_string(),
    };
    let display_str = format!("{}", err);
    assert!(display_str.contains("filter trap error"));
    assert!(display_str.contains("plugin crashed"));
}

#[test]
fn filter_error_implements_error_trait() {
    let err = FilterError::Runtime {
        reason: "test".to_string(),
    };
    let _: &dyn std::error::Error = &err;
}

#[test]
fn filter_error_debug_formatting() {
    let err = FilterError::Trap {
        reason: "test trap".to_string(),
    };
    let debug_str = format!("{:?}", err);
    assert!(debug_str.contains("Trap"));
    assert!(debug_str.contains("test trap"));
}

struct AllowAllFilter;

impl FilterPlugin for AllowAllFilter {
    fn filter(
        &self,
        _ctx: &RequestContext,
        _principal: &Principal,
        candidates: &[UpstreamCandidate],
    ) -> Result<FilterOutput, FilterError> {
        let ids: Vec<Uuid> = candidates.iter().map(|c| c.upstream_id).collect();
        Ok(FilterOutput {
            kept_upstream_ids: ids,
            reason: "all candidates allowed".to_string(),
            per_candidate_reasons: Vec::new(),
        })
    }

    fn plugin_id(&self) -> Uuid {
        Uuid::nil()
    }

    fn plugin_name(&self) -> &str {
        "allow-all"
    }
}

struct SelectiveFilter;

impl FilterPlugin for SelectiveFilter {
    fn filter(
        &self,
        _ctx: &RequestContext,
        _principal: &Principal,
        candidates: &[UpstreamCandidate],
    ) -> Result<FilterOutput, FilterError> {
        let filtered: Vec<Uuid> = candidates
            .iter()
            .filter(|c| c.name.contains("prod"))
            .map(|c| c.upstream_id)
            .collect();
        Ok(FilterOutput {
            kept_upstream_ids: filtered,
            reason: "filtered to production upstreams".to_string(),
            per_candidate_reasons: Vec::new(),
        })
    }

    fn plugin_id(&self) -> Uuid {
        Uuid::new_v4()
    }

    fn plugin_name(&self) -> &str {
        "selective"
    }
}

struct ErrorProducingFilter;

impl FilterPlugin for ErrorProducingFilter {
    fn filter(
        &self,
        _ctx: &RequestContext,
        _principal: &Principal,
        _candidates: &[UpstreamCandidate],
    ) -> Result<FilterOutput, FilterError> {
        Err(FilterError::Runtime {
            reason: "filter infrastructure failed".to_string(),
        })
    }

    fn plugin_id(&self) -> Uuid {
        Uuid::nil()
    }

    fn plugin_name(&self) -> &str {
        "error"
    }
}

#[test]
fn filter_plugin_object_safety() {
    let allow: Box<dyn FilterPlugin> = Box::new(AllowAllFilter);
    let selective: Box<dyn FilterPlugin> = Box::new(SelectiveFilter);
    let error: Box<dyn FilterPlugin> = Box::new(ErrorProducingFilter);

    let _plugins: Vec<Box<dyn FilterPlugin>> = vec![allow, selective, error];
}

#[test]
fn allow_all_filter_execution() {
    let filter = AllowAllFilter;
    let ctx = RequestContext {
        request_id: "req-1".to_string(),
        downstream_headers: http::HeaderMap::new(),
        method: Method::POST,
        path: "/v1/messages".to_string(),
        query: None,
        body_bytes: Bytes::new(),
        cache_breakpoints: Vec::new(),
        canonical_model_id: "claude-3-sonnet".to_string(),
    };
    let principal = Principal {
        id: "principal-1".to_string(),
        kind: PrincipalKind::ApiKey,
        claims: serde_json::json!({}).as_object().unwrap().clone(),
    };
    let id1 = Uuid::new_v4();
    let id2 = Uuid::new_v4();
    let candidates = vec![
        UpstreamCandidate {
            upstream_id: id1,
            name: "upstream-1".to_string(),
            kind: UpstreamKind::AnthropicApiKey,
            observed_rate_limits: Vec::new(),
            subscription_quotas: Vec::new(),
            observed_at_unix_secs: 0,
            cache_score: None,
            base_url: None,
        },
        UpstreamCandidate {
            upstream_id: id2,
            name: "upstream-2".to_string(),
            kind: UpstreamKind::AnthropicOauth,
            observed_rate_limits: Vec::new(),
            subscription_quotas: Vec::new(),
            observed_at_unix_secs: 0,
            cache_score: None,
            base_url: None,
        },
    ];

    let result = filter.filter(&ctx, &principal, &candidates);
    assert!(result.is_ok());
    let output = result.unwrap();
    assert_eq!(output.kept_upstream_ids.len(), 2);
    assert!(output.kept_upstream_ids.contains(&id1));
    assert!(output.kept_upstream_ids.contains(&id2));
}

#[test]
fn selective_filter_execution() {
    let filter = SelectiveFilter;
    let ctx = RequestContext {
        request_id: "req-2".to_string(),
        downstream_headers: http::HeaderMap::new(),
        method: Method::POST,
        path: "/v1/messages".to_string(),
        query: None,
        body_bytes: Bytes::new(),
        cache_breakpoints: Vec::new(),
        canonical_model_id: "claude-3-sonnet".to_string(),
    };
    let principal = Principal {
        id: "principal-2".to_string(),
        kind: PrincipalKind::OAuthSubject,
        claims: serde_json::json!({}).as_object().unwrap().clone(),
    };
    let prod_id = Uuid::new_v4();
    let staging_id = Uuid::new_v4();
    let candidates = vec![
        UpstreamCandidate {
            upstream_id: prod_id,
            name: "upstream-prod".to_string(),
            kind: UpstreamKind::AnthropicApiKey,
            observed_rate_limits: Vec::new(),
            subscription_quotas: Vec::new(),
            observed_at_unix_secs: 0,
            cache_score: None,
            base_url: None,
        },
        UpstreamCandidate {
            upstream_id: staging_id,
            name: "upstream-staging".to_string(),
            kind: UpstreamKind::AnthropicOauth,
            observed_rate_limits: Vec::new(),
            subscription_quotas: Vec::new(),
            observed_at_unix_secs: 0,
            cache_score: None,
            base_url: None,
        },
    ];

    let result = filter.filter(&ctx, &principal, &candidates);
    assert!(result.is_ok());
    let output = result.unwrap();
    assert_eq!(output.kept_upstream_ids.len(), 1);
    assert_eq!(output.kept_upstream_ids[0], prod_id);
}

#[test]
fn error_filter_execution() {
    let filter = ErrorProducingFilter;
    let ctx = RequestContext {
        request_id: "req-3".to_string(),
        downstream_headers: http::HeaderMap::new(),
        method: Method::POST,
        path: "/v1/messages".to_string(),
        query: None,
        body_bytes: Bytes::new(),
        cache_breakpoints: Vec::new(),
        canonical_model_id: "claude-3-sonnet".to_string(),
    };
    let principal = Principal {
        id: "principal-3".to_string(),
        kind: PrincipalKind::InternalKey,
        claims: serde_json::json!({}).as_object().unwrap().clone(),
    };
    let candidates = vec![];

    let result = filter.filter(&ctx, &principal, &candidates);
    assert!(result.is_err());
}

#[test]
fn filter_plugin_methods() {
    let filter = SelectiveFilter;
    let id = filter.plugin_id();
    let name = filter.plugin_name();
    assert_eq!(name, "selective");
    assert!(!id.is_nil());
}

#[test]
fn allow_all_filter_plugin_methods() {
    let filter = AllowAllFilter;
    let id = filter.plugin_id();
    let name = filter.plugin_name();
    assert_eq!(name, "allow-all");
    assert_eq!(id, Uuid::nil());
}

#[test]
fn terminal_strategy_default() {
    let default = TerminalStrategy::default();
    assert_eq!(default, TerminalStrategy::FirstPick);
}

#[test]
fn terminal_strategy_equality() {
    let first1 = TerminalStrategy::FirstPick;
    let first2 = TerminalStrategy::FirstPick;
    assert_eq!(first1, first2);

    let random = TerminalStrategy::Random;
    assert_ne!(first1, random);
}

#[test]
fn terminal_strategy_serialization() {
    let strategies = vec![
        TerminalStrategy::FirstPick,
        TerminalStrategy::Random,
        TerminalStrategy::RoundRobin,
        TerminalStrategy::LeastConnections,
    ];

    for strategy in strategies {
        let json = serde_json::to_string(&strategy).expect("serialization failed");
        let deserialized: TerminalStrategy =
            serde_json::from_str(&json).expect("deserialization failed");
        assert_eq!(strategy, deserialized);
    }
}

#[test]
fn terminal_strategy_serialization_format() {
    let first_pick = TerminalStrategy::FirstPick;
    let json = serde_json::to_string(&first_pick).unwrap();
    assert_eq!(json, "\"first-pick\"");

    let random = TerminalStrategy::Random;
    let json = serde_json::to_string(&random).unwrap();
    assert_eq!(json, "\"random\"");

    let round_robin = TerminalStrategy::RoundRobin;
    let json = serde_json::to_string(&round_robin).unwrap();
    assert_eq!(json, "\"round-robin\"");

    let least_conn = TerminalStrategy::LeastConnections;
    let json = serde_json::to_string(&least_conn).unwrap();
    assert_eq!(json, "\"least-connections\"");
}

#[test]
fn terminal_strategy_deserialization_from_kebab_case() {
    let json = "\"first-pick\"";
    let strategy: TerminalStrategy = serde_json::from_str(json).unwrap();
    assert_eq!(strategy, TerminalStrategy::FirstPick);

    let json = "\"random\"";
    let strategy: TerminalStrategy = serde_json::from_str(json).unwrap();
    assert_eq!(strategy, TerminalStrategy::Random);

    let json = "\"round-robin\"";
    let strategy: TerminalStrategy = serde_json::from_str(json).unwrap();
    assert_eq!(strategy, TerminalStrategy::RoundRobin);

    let json = "\"least-connections\"";
    let strategy: TerminalStrategy = serde_json::from_str(json).unwrap();
    assert_eq!(strategy, TerminalStrategy::LeastConnections);
}

#[test]
fn terminal_strategy_debug_formatting() {
    let first = TerminalStrategy::FirstPick;
    let debug_str = format!("{:?}", first);
    assert_eq!(debug_str, "FirstPick");

    let random = TerminalStrategy::Random;
    let debug_str = format!("{:?}", random);
    assert_eq!(debug_str, "Random");

    let round_robin = TerminalStrategy::RoundRobin;
    let debug_str = format!("{:?}", round_robin);
    assert_eq!(debug_str, "RoundRobin");

    let least_conn = TerminalStrategy::LeastConnections;
    let debug_str = format!("{:?}", least_conn);
    assert_eq!(debug_str, "LeastConnections");
}

#[test]
fn terminal_strategy_copy_clone() {
    let first = TerminalStrategy::FirstPick;
    let cloned = first.clone();
    let copied = first;
    assert_eq!(cloned, copied);
    assert_eq!(copied, TerminalStrategy::FirstPick);
}
