use crate::{
    http::CONTENT_TYPE_JSON,
    standards::indieauth::AccessToken,
    standards::micropub::{
        paging,
        query::{
            CategoryResponse, ConfigurationResponse, MatchingPropertyValuesMap, PostTypeInfo, Query, Response,
            SourceListResponse, SourceQuery, SourceResponse,
        },
    },
};
use http::header::CONTENT_TYPE;
use microformats::types::{Class, KnownClass};

use crate::{
    algorithms::ptd::Type,
    standards::micropub::{query::QueryKind, Error},
};

#[tracing_test::traced_test]
#[tokio::test]
async fn query_send() {
    let mut client = crate::test::Client::new().await;
    let token = AccessToken::new("a-bad-token");
    let query = crate::standards::micropub::query::Query {
        pagination: Default::default(),
        kind: QueryKind::Configuration,
    };
    let endpoint_mock = client
        .mock_server
        .mock("get", "/micropub/auth-failure")
        .match_query("q=config")
        .with_status(400)
        .with_header(CONTENT_TYPE.as_str(), CONTENT_TYPE_JSON)
        .with_body(
            serde_json::to_string(&serde_json::json!({
                "error": "invalid_request",
                "error_description": "This was a bad, bad request."
            }))
            .unwrap(),
        )
        .expect(1)
        .create_async()
        .await;

    let endpoint = format!("{}/micropub/auth-failure", client.mock_server.url())
        .parse()
        .unwrap();

    let query_result = query.send(&client, &endpoint, &token).await;

    endpoint_mock.assert_async().await;

    assert_eq!(
        query_result,
        Err(Error::bad_request("This was a bad, bad request").into()),
        "reports an authorization failure"
    );
}

#[test]
fn query_from_qs_str() {
    crate::test::Client::default();
    assert_eq!(
        Ok(vec![Type::Note, Type::Read]),
        serde_qs::from_str::<Query>(
            "q=source&post-type[]=note&post-type[]=read&channel=jump&limit=5"
        )
        .map_err(|e| e.to_string())
        .map(|q| q.kind)
        .map(|k| match k {
            QueryKind::Source(query) => query.post_type,
            _ => vec![],
        }),
        "provides deserialization of the source query with multiple post type asked for"
    );
    assert_eq!(
        Ok(vec![Type::Video, Type::Read]),
        serde_qs::Config::new(1, false)
            .deserialize_str::<Query>(
                "q=source&post-type%5B0%5D=video&post-type%5B1%5D=read&limit=5"
            )
            .map_err(|e| e.to_string())
            .map(|q| q.kind)
            .map(|k| match k {
                QueryKind::Source(query) => query.post_type,
                _ => vec![],
            }),
        "provides deserialization of the source query with multiple post type asked for"
    );

    assert_eq!(
        None,
        serde_qs::from_str::<Query>("q=source&post-type=")
            .err()
            .map(|e| e.to_string()),
        "ignores if the post type value is 'empty'"
    );

    assert_eq!(
        None,
        serde_qs::from_str::<Query>("q=source&syndicate-to=3")
            .err()
            .map(|e| e.to_string()),
        "supports filtering by syndication targets"
    );
    assert_eq!(
        None,
        serde_qs::from_str::<Query>("q=config")
            .err()
            .map(|e| e.to_string()),
        "provides deserialization of the config query"
    );
}

#[test]
fn query_from_qs_str_with_property_filtering() {
    crate::test::Client::default();
    assert_eq!(
        Ok(QueryKind::Source(Box::new(SourceQuery {
            exists: vec!["in-reply-to".to_string()],
            not_exists: vec!["like-of".to_string()],
            matching_properties: MatchingPropertyValuesMap::from_iter(vec![
                ("byline".into(), vec!["today".into()]),
                ("range".into(), vec!["3".into(), "10".into()])
            ]),
            ..Default::default()
        }))),
        serde_qs::from_str::<Query>("q=source&not-exists=like-of&exists=in-reply-to&property-byline=today&property-range[]=3&property-range[]=10")
            .map(|q| q.kind)
            .map_err(|e| e.to_string()),
        "provides deserialization of the config query"
    );
}

#[test]
fn query_to_str() {
    crate::test::Client::default();
    let result1 = serde_qs::to_string(&QueryKind::Source(Box::new(SourceQuery {
        post_type: vec![Type::Article],
        ..Default::default()
    })));

    assert_eq!(
        result1.as_ref().err().map(|s| s.to_string()),
        None,
        "can query a list of articles"
    );
    assert_eq!(
        Some("q=source&post-type=article".to_string()),
        result1.ok(),
        "can query a list of articles"
    );

    let result2 = serde_qs::to_string(&QueryKind::Source(Box::new(SourceQuery {
        post_type: vec![Type::Article, Type::Note],
        ..Default::default()
    })));
    assert_eq!(
        Some("q=source&post-type[0]=article&post-type[1]=note".to_string()),
        result2.ok(),
        "can query a list of articles and notes"
    );
}

#[test]
#[cfg(feature = "experimental_channels")]
fn query_response_for_channels() {
    use super::extension;

    assert_eq!(
        Some(Response::Channel(extension::channel::QueryResponse {
            channels: vec![extension::channel::Form::Expanded {
                uid: "magic".to_string(),
                name: "Magic".to_string(),
                properties: serde_json::json!({"grr": "bark"}).try_into().unwrap()
            }],
            paging: Default::default()
        })),
        serde_json::from_value(serde_json::json!({
            "channels": [{
                "uid": "magic",
                "name": "Magic",
                "grr": "bark"
            }]
        }))
        .ok()
    )
}

#[test]
fn query_response_for_configuration() {
    assert_eq!(
        Ok(Response::Configuration(ConfigurationResponse {
            q: vec!["channels".to_owned()],
            category: vec!["tag".into()],
            media_endpoint: None,
            post_types: vec![PostTypeInfo::Simple(Type::Note)],
            channels: Default::default(),
            syndicate_to: Default::default(),
            extensions: Default::default()
        })),
        serde_json::from_value(serde_json::json!({
            "q": ["channels"],
            "post-types": ["note"],
            "category": ["tag"]
        }))
        .map(Response::Configuration)
        .map_err(crate::Error::JSON)
    );
}

#[test]
#[cfg(feature = "experimental_syndication")]
fn query_response_for_configuration_with_syndication() {
    use crate::standards::micropub::extension;

    assert_eq!(
        Ok(Response::Configuration(ConfigurationResponse {
            q: vec!["channels".to_owned()],
            category: vec!["tag".into()],
            media_endpoint: None,
            post_types: vec![PostTypeInfo::Simple(Type::Note)],
            channels: Default::default(),
            syndicate_to: vec![extension::syndication::Target {
                uid: "magic".into(),
                name: "cookie".into(),
                ..Default::default()
            }],
            extensions: Default::default()
        })),
        serde_json::from_value(serde_json::json!({
            "q": ["channels"],
            "post-types": ["note"],
            "category": ["tag"],
            "syndicate-to": [
                {
                    "uid": "magic",
                    "name": "cookie"
                }
            ]
        }))
        .map(Response::Configuration)
        .map_err(crate::Error::JSON)
    );
}

#[test]
#[cfg(feature = "experimental_channels")]
fn query_response_for_configuration_with_channels() {
    assert_eq!(
        Ok(Response::Configuration(ConfigurationResponse {
            q: vec!["channels".to_owned()],
            category: vec!["tag".into()],
            media_endpoint: None,
            post_types: vec![PostTypeInfo::Simple(Type::Note)],
            channels: Default::default(),
            syndicate_to: Default::default(),
            extensions: Default::default()
        })),
        serde_json::from_value(serde_json::json!({
            "q": ["channels"],
            "post-types": ["note"],
            "category": ["tag"]
        }))
        .map(Response::Configuration)
        .map_err(crate::Error::JSON)
    );
}

#[test]
fn query_response_for_categories() {
    assert_eq!(
        Ok(Response::Category(CategoryResponse {
            categories: vec!["jump".into(), "kick".into(), "spin".into()],
            pagination: Default::default()
        })),
        serde_json::from_value(serde_json::json!({
            "categories": ["jump", "kick", "spin"]
        }))
        .map_err(|e| format!("{:#?}", e)),
        "works without paging"
    );
}

#[test]
fn query_response_for_categories_with_paging() {
    use super::extension::Order;

    assert_eq!(
        Ok(CategoryResponse {
            categories: vec!["jump".into(), "kick".into(), "spin".into()],
            pagination: paging::Fields {
                paging: paging::Query {
                    order: Some(Order::Descending),
                    ..Default::default()
                },
                ..Default::default()
            }
        }),
        serde_json::from_value(serde_json::json!({
            "categories": ["jump", "kick", "spin"], "paging": { "order": "desc" }
        }))
        .map_err(|e| format!("{:#?}", e)),
        "works with paging"
    )
}

#[test]
fn query_response_for_source() {
    crate::test::Client::default();
    let item = microformats::types::Item {
        r#type: vec![Class::Known(KnownClass::Entry)],
        ..Default::default()
    };
    assert_eq!(
        Ok(Response::Source(SourceResponse {
            post_type: vec![],
            item
        })),
        serde_json::from_value(serde_json::json!({
            "type": ["h-entry"],
            "properties": {}
        }))
        .map_err(|e| e.to_string())
    );

    assert_eq!(
        serde_json::from_value::<Response>(serde_json::json!(
        {
            "post-type": [
                "article"
            ],
            "properties": {
                "audience": [],
                "category": [],
                "channel": [
                    "all"
                ],
                "content": {
                    "html": "<p>well-here-we-go</p>"
                },
                "name": "magic-omg",
                "post-status": [
                    "published"
                ],
                "published": [
                    "2022-02-12T23:22:27+00:00"
                ],
                "slug": [
                    "Gzg043ii"
                ],
                "syndication": [],
                "updated": [
                    "2022-02-12T23:22:27+00:00"
                ],
                "url": [
                    "http://localhost:3112/Gzg043ii"
                ],
                "visibility": [
                    "public"
                ]
            },
            "type": [
                "h-entry"
            ]
        }
                ))
        .map_err(|e| e.to_string())
        .err(),
        None,
    )
}

// Tests for Task 3: PostTypeInfo
#[test]
fn post_type_info_simple_format() {
    // Test backward compatibility: simple string format
    let json = serde_json::json!(["note", "article"]);
    let result: Result<Vec<PostTypeInfo>, _> = serde_json::from_value(json);
    
    assert_eq!(
        result.ok(),
        Some(vec![
            PostTypeInfo::Simple(Type::Note),
            PostTypeInfo::Simple(Type::Article)
        ])
    );
}

#[test]
fn post_type_info_extended_format() {
    // Test extended format with metadata
    let json = serde_json::json!([
        {
            "type": "note",
            "name": "Note",
            "properties": ["content", "category", "published"]
        },
        {
            "type": "article",
            "name": "Article"
        }
    ]);
    
    let result: Result<Vec<PostTypeInfo>, _> = serde_json::from_value(json);
    assert!(result.is_ok());
    
    let types = result.unwrap();
    assert_eq!(types.len(), 2);
    
    match &types[0] {
        PostTypeInfo::Extended { r#type, name, properties } => {
            assert_eq!(r#type, &Type::Note);
            assert_eq!(name, "Note");
            assert_eq!(
                properties.as_ref().unwrap(),
                &vec!["content".to_string(), "category".to_string(), "published".to_string()]
            );
        }
        _ => panic!("Expected Extended variant"),
    }
    
    match &types[1] {
        PostTypeInfo::Extended { r#type, name, properties } => {
            assert_eq!(r#type, &Type::Article);
            assert_eq!(name, "Article");
            assert_eq!(properties, &None);
        }
        _ => panic!("Expected Extended variant"),
    }
}

#[test]
fn post_type_info_serialization() {
    // Test that Simple serializes back to simple string
    let simple = PostTypeInfo::Simple(Type::Note);
    let json = serde_json::to_value(&simple).unwrap();
    assert_eq!(json, serde_json::json!("note"));
    
    // Test that Extended serializes to object
    let extended = PostTypeInfo::Extended {
        r#type: Type::Article,
        name: "Article".to_string(),
        properties: Some(vec!["content".to_string(), "name".to_string()]),
    };
    let json = serde_json::to_value(&extended).unwrap();
    assert_eq!(
        json,
        serde_json::json!({
            "type": "article",
            "name": "Article",
            "properties": ["content", "name"]
        })
    );
}

#[test]
fn config_response_with_extended_post_types() {
    // Test ConfigurationResponse with extended post-types
    let json = serde_json::json!({
        "post-types": [
            {
                "type": "note",
                "name": "Note",
                "properties": ["content", "category"]
            },
            {
                "type": "article",
                "name": "Article",
                "properties": ["content", "name", "summary"]
            }
        ],
        "media-endpoint": "https://example.com/media"
    });
    
    let result: Result<ConfigurationResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok());
    
    let config = result.unwrap();
    assert_eq!(config.post_types.len(), 2);
    assert_eq!(config.media_endpoint, Some("https://example.com/media".parse().unwrap()));
}

#[test]
fn config_response_backward_compatible() {
    // Test that old simple format still works
    let json = serde_json::json!({
        "post-types": ["note", "article", "photo"]
    });
    
    let result: Result<ConfigurationResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok());
    
    let config = result.unwrap();
    assert_eq!(config.post_types.len(), 3);
    
    // All should be Simple variants
    for pt in &config.post_types {
        assert!(matches!(pt, PostTypeInfo::Simple(_)));
    }
}

// Tests for Task 4: SourceListResponse
#[test]
fn source_list_response_basic() {
    use microformats::types::{Class, KnownClass};
    
    let json = serde_json::json!({
        "items": [
            {
                "type": ["h-entry"],
                "properties": {
                    "content": ["Hello world"]
                }
            },
            {
                "type": ["h-entry"],
                "properties": {
                    "content": ["Second post"]
                }
            }
        ]
    });
    
    let result: Result<SourceListResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok());
    
    let list = result.unwrap();
    assert_eq!(list.items.len(), 2);
    assert_eq!(list.items[0].item.r#type, vec![Class::Known(KnownClass::Entry)]);
}

#[test]
fn source_list_response_with_paging() {
    let json = serde_json::json!({
        "items": [
            {
                "type": ["h-entry"],
                "properties": {}
            }
        ],
        "paging": {
            "after": "cursor_token_123"
        }
    });
    
    let result: Result<SourceListResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok());
    
    let list = result.unwrap();
    assert_eq!(list.items.len(), 1);
    assert!(list.paging.paging.after.is_some());
    assert_eq!(list.paging.paging.after.unwrap(), "cursor_token_123");
}

#[test]
fn source_list_response_in_response_enum() {
    use microformats::types::{Class, KnownClass};

    let json = serde_json::json!({
        "items": [
            {
                "type": ["h-entry"],
                "properties": {
                    "content": ["Test post"]
                }
            }
        ]
    });
    
    let result: Result<Response, _> = serde_json::from_value(json);
    assert!(result.is_ok(), "Failed to deserialize as Response");
    
    match result.unwrap() {
        Response::SourceList(list) => {
            assert_eq!(list.items.len(), 1);
            assert_eq!(list.items[0].item.r#type, vec![Class::Known(KnownClass::Entry)]);
        }
        other => panic!("Expected SourceList variant, got {:?}", other),
    }
}

#[test]
fn source_list_empty_items() {
    let json = serde_json::json!({
        "items": []
    });
    
    let result: Result<SourceListResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok(), "Failed to deserialize empty items list");
    
    let list = result.unwrap();
    assert_eq!(list.items.len(), 0);
}

#[test]
fn source_list_response_serialization() {
    use microformats::types::{Class, KnownClass, Item};
    
    let list = SourceListResponse {
        items: vec![
            SourceResponse {
                post_type: vec![Type::Note],
                item: Item {
                    r#type: vec![Class::Known(KnownClass::Entry)],
                    properties: std::collections::BTreeMap::new(),
                    ..Default::default()
                },
            }
        ],
        paging: Default::default(),
    };
    
    let json = serde_json::to_value(&list).unwrap();
    assert!(json.get("items").is_some());
    assert!(json.get("items").unwrap().is_array());
    assert_eq!(json.get("items").unwrap().as_array().unwrap().len(), 1);
}

// ============================================================================
// Tests for SyndicationTargetsResponse (syndicate-to query)
// ============================================================================

#[test]
fn syndication_targets_response_basic() {
    let json = serde_json::json!({
        "syndicate-to": [
            {
                "uid": "https://twitter.com/user",
                "name": "Twitter"
            },
            {
                "uid": "https://facebook.com/user",
                "name": "Facebook"
            }
        ]
    });

    let result: Result<super::SyndicationTargetsResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok(), "Failed to deserialize syndication targets");

    let response = result.unwrap();
    assert_eq!(response.syndicate_to.len(), 2);
    assert_eq!(response.syndicate_to[0].uid, "https://twitter.com/user");
    assert_eq!(response.syndicate_to[0].name, "Twitter");
}

#[test]
#[cfg(feature = "experimental_syndication")]
fn syndication_targets_response_with_full_info() {
    let json = serde_json::json!({
        "syndicate-to": [
            {
                "uid": "https://twitter.com/user",
                "name": "Twitter"
            },
            {
                "uid": "https://facebook.com/user",
                "name": "Facebook"
            }
        ]
    });
    
    let result: Result<super::SyndicationTargetsResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok(), "Failed to deserialize syndication targets");
    
    let response = result.unwrap();
    assert_eq!(response.syndicate_to.len(), 2);
    assert_eq!(response.syndicate_to[0].uid, "https://twitter.com/user");
    assert_eq!(response.syndicate_to[0].name, "Twitter");
}

#[test]
#[cfg(feature = "experimental_syndication")]
fn syndication_targets_empty() {
    let json = serde_json::json!({
        "syndicate-to": []
    });
    
    let result: Result<super::SyndicationTargetsResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok(), "Failed to deserialize empty syndication targets");
    
    let response = result.unwrap();
    assert_eq!(response.syndicate_to.len(), 0);
}

#[test]
#[cfg(feature = "experimental_syndication")]
fn syndication_targets_response_serialization() {
    use crate::standards::micropub::extension::syndication;
    
    let response = super::SyndicationTargetsResponse {
        syndicate_to: vec![
            syndication::Target {
                uid: "https://twitter.com/user".to_string(),
                name: "Twitter".to_string(),
                service: None,
                user: None,
            }
        ],
    };
    
    let json = serde_json::to_value(&response).unwrap();
    assert!(json.get("syndicate-to").is_some());
    assert!(json.get("syndicate-to").unwrap().is_array());
    assert_eq!(json.get("syndicate-to").unwrap().as_array().unwrap().len(), 1);
}

#[test]
#[cfg(feature = "experimental_syndication")]
fn syndication_targets_in_response_enum() {
    let json = serde_json::json!({
        "syndicate-to": [
            {
                "uid": "https://twitter.com/user",
                "name": "Twitter"
            }
        ]
    });
    
    let result: Result<super::Response, _> = serde_json::from_value(json);
    assert!(result.is_ok(), "Failed to deserialize as Response");
    
    match result.unwrap() {
        super::Response::SyndicateTo(targets) => {
            assert_eq!(targets.syndicate_to.len(), 1);
            assert_eq!(targets.syndicate_to[0].name, "Twitter");
        }
        other => panic!("Expected SyndicateTo variant, got {:?}", other),
    }
}

// ============================================================================
// Tests for extensions field in ConfigurationResponse
// ============================================================================

#[test]
fn config_response_extensions_appear_when_present() {
    // Test that extensions appear in serialized output when populated
    let config = ConfigurationResponse {
        q: vec!["source".to_string()],
        category: vec![],
        media_endpoint: None,
        post_types: vec![],
        #[cfg(feature = "experimental_channels")]
        channels: vec![],
        #[cfg(feature = "experimental_syndication")]
        syndicate_to: vec![],
        extensions: vec!["batch".to_string(), "channels".to_string()],
    };

    let json = serde_json::to_value(&config).unwrap();
    assert_eq!(
        json.get("extensions"),
        Some(&serde_json::json!(["batch", "channels"])),
        "extensions should appear in JSON when populated"
    );
}

#[test]
fn config_response_extensions_skipped_when_empty() {
    // Test that extensions are not serialized when empty
    let config = ConfigurationResponse {
        q: vec!["source".to_string()],
        category: vec![],
        media_endpoint: None,
        post_types: vec![],
        #[cfg(feature = "experimental_channels")]
        channels: vec![],
        #[cfg(feature = "experimental_syndication")]
        syndicate_to: vec![],
        extensions: vec![],
    };

    let json = serde_json::to_value(&config).unwrap();
    assert_eq!(
        json.get("extensions"),
        None,
        "extensions should be skipped when empty"
    );
}

#[test]
fn config_response_extensions_deserialization() {
    // Test that extensions can be deserialized from JSON
    let json = serde_json::json!({
        "q": ["source", "config"],
        "extensions": ["batch", "media-query", "custom-extension"]
    });

    let result: Result<ConfigurationResponse, _> = serde_json::from_value(json);
    assert!(result.is_ok(), "Failed to deserialize config with extensions");

    let config = result.unwrap();
    assert_eq!(config.extensions.len(), 3);
    assert!(config.extensions.contains(&"batch".to_string()));
    assert!(config.extensions.contains(&"media-query".to_string()));
    assert!(config.extensions.contains(&"custom-extension".to_string()));
}

#[test]
fn config_response_extensions_roundtrip() {
    // Test full roundtrip serialization/deserialization
    let original = ConfigurationResponse {
        q: vec!["source".to_string(), "config".to_string()],
        category: vec!["tag".to_string()],
        media_endpoint: Some("https://example.com/media".parse().unwrap()),
        post_types: vec![PostTypeInfo::Simple(Type::Note)],
        #[cfg(feature = "experimental_channels")]
        channels: vec![],
        #[cfg(feature = "experimental_syndication")]
        syndicate_to: vec![],
        extensions: vec!["batch".to_string(), "publish-delay".to_string()],
    };

    let json = serde_json::to_value(&original).unwrap();
    let deserialized: ConfigurationResponse = serde_json::from_value(json).unwrap();

    assert_eq!(original.extensions, deserialized.extensions);
    assert_eq!(original.q, deserialized.q);
    assert_eq!(original.category, deserialized.category);
}
