use std::error::Error as StdError;

use rich_error::RichError;
use thiserror::Error as ThisError;

#[derive(Debug, ThisError, RichError)]
pub enum Inner {
    #[error("database error: {0}")]
    Database(DbError),

    #[error("invalid transition from {operation}")]
    InvalidOperation { operation: &'static str },

    #[error("simple unit error")]
    SimpleUnit,
}

#[derive(Debug)]
pub struct DbError(&'static str);

impl std::fmt::Display for DbError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl StdError for DbError {}

#[derive(Debug, ThisError, RichError)]
#[rich_error(outer = BridgeError)]
pub enum RenamedInner {
    #[error("renamed tuple error: {0}")]
    Tuple(DbError),

    #[error("renamed struct error: {context}")]
    StructVariant {
        #[source]
        source: DbError,
        context: &'static str,
    },

    #[error("renamed unit error")]
    RenamedUnit,
}

#[test]
fn constructors_capture_call_site_and_variant() {
    let expected_line = line!() + 1;
    let err = Error::invalid_operation("pending");

    assert!(
        matches!(err.inner(), Inner::InvalidOperation { operation } if *operation == "pending"),
        "struct variant constructor should populate fields"
    );
    assert!(
        err.location().file().ends_with("tests/rich_error.rs"),
        "location should reference this test file"
    );
    assert_eq!(
        err.location().line(),
        expected_line,
        "location should capture the call site line"
    );

    let rendered = err.to_string();
    assert!(
        rendered.starts_with("invalid transition from pending"),
        "Display should include inner error message"
    );
    assert!(
        rendered.contains("(at "),
        "Display should include location metadata"
    );
}

#[test]
fn single_field_variants_get_from_impls() {
    let err: Error = DbError("offline").into();

    match err.inner() {
        Inner::Database(db_err) => assert_eq!(db_err.0, "offline"),
        _ => panic!("expected database variant via From<FieldTy> impl"),
    }
}

#[test]
fn source_delegates_to_inner_error() {
    let err = Error::simple_unit();
    let source = StdError::source(&err).expect("outer error should expose inner error");
    assert_eq!(source.to_string(), "simple unit error");
}

#[test]
fn outer_error_name_can_be_overridden() {
    let tuple = BridgeError::tuple(DbError("tuple"));
    match tuple.inner() {
        RenamedInner::Tuple(db) => assert_eq!(db.0, "tuple"),
        _ => panic!("tuple constructor routed to wrong variant"),
    }

    let struct_err = BridgeError::struct_variant(DbError("struct"), "ctx");
    match struct_err.inner() {
        RenamedInner::StructVariant { source, context } => {
            assert_eq!(source.0, "struct");
            assert_eq!(*context, "ctx");
        }
        _ => panic!("struct constructor routed to wrong variant"),
    }

    let unit = BridgeError::renamed_unit();
    match unit.inner() {
        RenamedInner::RenamedUnit => {}
        _ => panic!("unit constructor routed to wrong variant"),
    }
}
