/// Authorship test helper utilities
use std::fs;
use std::path::Path;

use indieweb::algorithms::authorship::Author;
use indieweb::http::{Body, Client};
use indieweb::mf2::http::to_mf2_document;
use microformats::types::Document;

/// Load HTML fixture from tests/fixtures/authorship/
pub fn load_fixture(name: &str) -> String {
    let fixture_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/authorship")
        .join(name);
    fs::read_to_string(&fixture_path)
        .unwrap_or_else(|e| panic!("Failed to load fixture {:?}: {}", fixture_path, e))
}

/// A mock HTTP client that serves responses from a local map.
#[derive(Debug, Default)]
pub struct MockClient {
    responses: Vec<(String, String)>,
}

impl MockClient {
    pub fn add_response(&mut self, url: String, body: String) {
        self.responses.push((url, body));
    }
}

#[async_trait::async_trait]
impl Client for MockClient {
    async fn send_request(
        &self,
        request: ::http::Request<Body>,
    ) -> Result<::http::Response<Body>, indieweb::Error> {
        let uri = request.uri().to_string();
        let found = self
            .responses
            .iter()
            .find(|(u, _)| uri.starts_with(u))
            .map(|(_, b)| b.clone());

        if let Some(body) = found {
            Ok(::http::Response::builder()
                .status(200)
                .header("content-type", "text/html")
                .body(Body::Bytes(body.as_bytes().to_vec()))
                .unwrap())
        } else {
            Ok(::http::Response::builder()
                .status(404)
                .body(Body::Empty)
                .unwrap())
        }
    }
}

/// Parse HTML into a Microformats2 Document.
pub fn parse_html_to_document(html: &str, base_url: &str) -> Document {
    let resp = ::http::Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(Body::Bytes(html.as_bytes().to_vec()))
        .unwrap();

    to_mf2_document(resp.map(|b| b.as_bytes().to_vec()), base_url)
        .expect("Failed to parse HTML as Microformats2")
}

/// Find the first h-entry in a document (including nested in children)
pub fn find_h_entry(document: &Document) -> Option<&microformats::types::Item> {
    use microformats::types::{Class, KnownClass};

    fn find_in_items(items: &[microformats::types::Item]) -> Option<&microformats::types::Item> {
        for item in items {
            if item.r#type.contains(&Class::Known(KnownClass::Entry)) {
                return Some(item);
            }
            if let Some(found) = find_in_items(&item.children) {
                return Some(found);
            }
        }
        None
    }

    find_in_items(&document.items)
}

/// Find the first h-feed in a document
pub fn find_h_feed(document: &Document) -> Option<&microformats::types::Item> {
    use microformats::types::{Class, KnownClass};

    document
        .items
        .iter()
        .find(|item| item.r#type.contains(&Class::Known(KnownClass::Feed)))
}

/// Expected author result for assertions
#[derive(Debug, PartialEq, Clone)]
pub struct ExpectedAuthor {
    pub name: String,
    pub url: Option<String>,
    pub photo: Option<String>,
}

impl ExpectedAuthor {
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            url: None,
            photo: None,
        }
    }

    pub fn with_url(mut self, url: &str) -> Self {
        self.url = Some(url.to_string());
        self
    }

    pub fn with_photo(mut self, photo: &str) -> Self {
        self.photo = Some(photo.to_string());
        self
    }
}

/// Helper for asserting authorship discovery results
pub trait AuthorshipAssertions {
    fn assert_author_matches(&self, expected: &ExpectedAuthor);
    fn assert_name(&self, expected_name: &str);
    fn assert_no_author(&self);
}

impl AuthorshipAssertions for Result<Author, indieweb::algorithms::authorship::AuthorshipError> {
    fn assert_author_matches(&self, expected: &ExpectedAuthor) {
        assert!(self.is_ok(), "Expected author but got error: {:?}", self);
        let author = self.as_ref().unwrap();
        assert_eq!(author.name, expected.name, "Author name mismatch");

        match (&expected.url, &author.url) {
            (Some(expected_url), Some(actual_url)) => {
                assert_eq!(actual_url.as_str(), expected_url, "Author URL mismatch");
            }
            (None, None) => {}
            (expected, actual) => {
                panic!(
                    "Author URL mismatch: expected {:?}, got {:?}",
                    expected, actual
                );
            }
        }

        match (&expected.photo, &author.photo) {
            (Some(expected_photo), Some(actual_photo)) => {
                assert_eq!(
                    actual_photo.as_str(),
                    expected_photo,
                    "Author photo mismatch"
                );
            }
            (None, None) => {}
            (expected, actual) => {
                panic!(
                    "Author photo mismatch: expected {:?}, got {:?}",
                    expected, actual
                );
            }
        }
    }

    fn assert_name(&self, expected_name: &str) {
        assert!(self.is_ok(), "Expected author but got error: {:?}", self);
        let author = self.as_ref().unwrap();
        assert_eq!(author.name, expected_name, "Author name mismatch");
    }

    fn assert_no_author(&self) {
        use indieweb::algorithms::authorship::AuthorshipError;
        assert!(
            matches!(self, Err(AuthorshipError::NoAuthorFound)),
            "Expected NoAuthorFound error but got: {:?}",
            self
        );
    }
}
