use std::collections::HashMap;

use ed25519_dalek::SigningKey;
use ontoref_triples::TripleStore;
use ontoref_types::{
    operation::{OpBody, OpPayload, Operation},
    AttrId, EntityId, Hlc, PublicKey, Value,
};

fn signing_key() -> SigningKey {
    SigningKey::from_bytes(&[9u8; 32])
}

fn assert_op(
    entity: &str,
    attrs: Vec<(&str, Value)>,
    tx: Hlc,
    valid_from: Option<Hlc>,
) -> Operation {
    let key = signing_key();
    let body = OpBody {
        actor: PublicKey::from_signing_key(&key),
        parents: vec![],
        payload: OpPayload::EntityAssert {
            attrs: attrs
                .into_iter()
                .map(|(name, value)| (AttrId::new(name), value))
                .collect(),
            entity: EntityId::new(entity),
            valid_from,
        },
        timestamp: tx,
    };
    Operation::sign(body, &key)
}

#[test]
fn forward_index_returns_attrs_for_entity() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("triples.redb")).unwrap();

    let tx = Hlc::new(1, 0);
    store
        .apply(&assert_op(
            "alice",
            vec![
                ("name", Value::Str("Alice".into())),
                ("age", Value::Int(30)),
            ],
            tx,
            None,
        ))
        .unwrap();

    let attrs = store.entity_attrs(&EntityId::new("alice"), tx, tx).unwrap();
    let map: HashMap<String, Value> = attrs
        .into_iter()
        .map(|(attr, value)| (attr.as_str().to_string(), value))
        .collect();
    assert_eq!(map.get("name"), Some(&Value::Str("Alice".into())));
    assert_eq!(map.get("age"), Some(&Value::Int(30)));
}

#[test]
fn forward_index_picks_latest_assertion_per_attr() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("t.redb")).unwrap();

    store
        .apply(&assert_op(
            "e",
            vec![("color", Value::Str("red".into()))],
            Hlc::new(1, 0),
            None,
        ))
        .unwrap();
    store
        .apply(&assert_op(
            "e",
            vec![("color", Value::Str("blue".into()))],
            Hlc::new(2, 0),
            None,
        ))
        .unwrap();

    let attrs = store
        .entity_attrs(&EntityId::new("e"), Hlc::new(10, 0), Hlc::new(10, 0))
        .unwrap();
    assert_eq!(attrs.len(), 1);
    assert_eq!(attrs[0].1, Value::Str("blue".into()));
}

#[test]
fn reverse_index_finds_entities_by_value() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("t.redb")).unwrap();

    store
        .apply(&assert_op(
            "e1",
            vec![("color", Value::Str("red".into()))],
            Hlc::new(1, 0),
            None,
        ))
        .unwrap();
    store
        .apply(&assert_op(
            "e2",
            vec![("color", Value::Str("red".into()))],
            Hlc::new(2, 0),
            None,
        ))
        .unwrap();
    store
        .apply(&assert_op(
            "e3",
            vec![("color", Value::Str("blue".into()))],
            Hlc::new(3, 0),
            None,
        ))
        .unwrap();

    let red = store
        .entities_by_value(
            &AttrId::new("color"),
            &Value::Str("red".into()),
            Hlc::new(10, 0),
            Hlc::new(10, 0),
        )
        .unwrap();
    assert_eq!(red, vec![EntityId::new("e1"), EntityId::new("e2")]);

    let blue = store
        .entities_by_value(
            &AttrId::new("color"),
            &Value::Str("blue".into()),
            Hlc::new(10, 0),
            Hlc::new(10, 0),
        )
        .unwrap();
    assert_eq!(blue, vec![EntityId::new("e3")]);
}

#[test]
fn reverse_index_excludes_overwritten_entities() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("t.redb")).unwrap();

    store
        .apply(&assert_op(
            "e1",
            vec![("color", Value::Str("red".into()))],
            Hlc::new(1, 0),
            None,
        ))
        .unwrap();
    store
        .apply(&assert_op(
            "e1",
            vec![("color", Value::Str("blue".into()))],
            Hlc::new(2, 0),
            None,
        ))
        .unwrap();

    let red_now = store
        .entities_by_value(
            &AttrId::new("color"),
            &Value::Str("red".into()),
            Hlc::new(10, 0),
            Hlc::new(10, 0),
        )
        .unwrap();
    assert!(
        red_now.is_empty(),
        "e1 was overwritten to blue; reverse query for red must exclude it"
    );

    let blue_now = store
        .entities_by_value(
            &AttrId::new("color"),
            &Value::Str("blue".into()),
            Hlc::new(10, 0),
            Hlc::new(10, 0),
        )
        .unwrap();
    assert_eq!(blue_now, vec![EntityId::new("e1")]);
}

#[test]
fn as_of_filters_to_tx_time_horizon() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("t.redb")).unwrap();

    let tx1 = Hlc::new(10, 0);
    let tx2 = Hlc::new(20, 0);

    store
        .apply(&assert_op("e", vec![("v", Value::Int(1))], tx1, None))
        .unwrap();
    store
        .apply(&assert_op("e", vec![("v", Value::Int(2))], tx2, None))
        .unwrap();

    let at_tx1 = store.entity_attrs(&EntityId::new("e"), tx1, tx1).unwrap();
    assert_eq!(at_tx1[0].1, Value::Int(1));

    let at_tx2 = store.entity_attrs(&EntityId::new("e"), tx2, tx2).unwrap();
    assert_eq!(at_tx2[0].1, Value::Int(2));
}

#[test]
fn as_of_with_old_horizon_excludes_future_assertions() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("t.redb")).unwrap();

    let tx1 = Hlc::new(10, 0);
    let tx2 = Hlc::new(20, 0);

    store
        .apply(&assert_op("e", vec![("v", Value::Int(1))], tx1, None))
        .unwrap();
    store
        .apply(&assert_op("e", vec![("v", Value::Int(2))], tx2, None))
        .unwrap();

    let view = store
        .entity_attrs(&EntityId::new("e"), Hlc::new(15, 0), Hlc::new(15, 0))
        .unwrap();
    assert_eq!(view[0].1, Value::Int(1));
}

#[test]
fn bitemporal_distinguishes_tx_time_from_valid_time() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("t.redb")).unwrap();

    let tx = Hlc::new(100, 0);
    let valid_from = Hlc::new(5, 0);
    store
        .apply(&assert_op(
            "alice",
            vec![("name", Value::Str("Alice".into()))],
            tx,
            Some(valid_from),
        ))
        .unwrap();

    let after_valid = store
        .entity_attr(
            &EntityId::new("alice"),
            &AttrId::new("name"),
            tx,
            Hlc::new(10, 0),
        )
        .unwrap();
    assert_eq!(after_valid, Some(Value::Str("Alice".into())));

    let before_valid = store
        .entity_attr(
            &EntityId::new("alice"),
            &AttrId::new("name"),
            tx,
            Hlc::new(1, 0),
        )
        .unwrap();
    assert_eq!(before_valid, None);
}

#[test]
fn bitemporal_valid_time_picks_value_in_effect() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("t.redb")).unwrap();

    // Two retroactive corrections recorded at different tx_times but with
    // different valid_from points.
    store
        .apply(&assert_op(
            "e",
            vec![("v", Value::Str("A".into()))],
            Hlc::new(100, 0),
            Some(Hlc::new(10, 0)),
        ))
        .unwrap();
    store
        .apply(&assert_op(
            "e",
            vec![("v", Value::Str("B".into()))],
            Hlc::new(101, 0),
            Some(Hlc::new(20, 0)),
        ))
        .unwrap();

    let between = store
        .entity_attr(
            &EntityId::new("e"),
            &AttrId::new("v"),
            Hlc::new(200, 0),
            Hlc::new(15, 0),
        )
        .unwrap();
    assert_eq!(between, Some(Value::Str("A".into())));

    let after = store
        .entity_attr(
            &EntityId::new("e"),
            &AttrId::new("v"),
            Hlc::new(200, 0),
            Hlc::new(25, 0),
        )
        .unwrap();
    assert_eq!(after, Some(Value::Str("B".into())));
}

fn retract_op(entity: &str, attr_names: &[&str], tx: Hlc) -> Operation {
    let key = signing_key();
    let body = OpBody {
        actor: PublicKey::from_signing_key(&key),
        parents: vec![],
        payload: OpPayload::Retract {
            attrs: attr_names.iter().map(|n| AttrId::new(*n)).collect(),
            entity: EntityId::new(entity),
        },
        timestamp: tx,
    };
    Operation::sign(body, &key)
}

#[test]
fn retract_hides_attribute_in_query_view() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("triples.redb")).unwrap();

    // Assert e.v = 1 at tx=10, then retract e.v at tx=20.
    store
        .apply(&assert_op(
            "e",
            vec![("v", Value::Int(1))],
            Hlc::new(10, 0),
            None,
        ))
        .unwrap();
    store
        .apply(&retract_op("e", &["v"], Hlc::new(20, 0)))
        .unwrap();

    // As of tx=20 the attribute is gone.
    let after = store
        .entity_attrs(&EntityId::new("e"), Hlc::new(20, 0), Hlc::new(20, 0))
        .unwrap();
    assert!(after.is_empty(), "a winning retraction hides the attribute");

    // Bitemporal: as of tx=10 (before the retract) it is still visible.
    let before = store
        .entity_attrs(&EntityId::new("e"), Hlc::new(10, 0), Hlc::new(10, 0))
        .unwrap();
    assert_eq!(before, vec![(AttrId::new("v"), Value::Int(1))]);
}

#[test]
fn reassert_after_retract_restores_attribute() {
    let dir = tempfile::tempdir().unwrap();
    let store = TripleStore::open(dir.path().join("triples.redb")).unwrap();
    store
        .apply(&assert_op(
            "e",
            vec![("v", Value::Int(1))],
            Hlc::new(10, 0),
            None,
        ))
        .unwrap();
    store
        .apply(&retract_op("e", &["v"], Hlc::new(20, 0)))
        .unwrap();
    store
        .apply(&assert_op(
            "e",
            vec![("v", Value::Int(2))],
            Hlc::new(30, 0),
            None,
        ))
        .unwrap();

    let now = store
        .entity_attrs(&EntityId::new("e"), Hlc::new(30, 0), Hlc::new(30, 0))
        .unwrap();
    assert_eq!(
        now,
        vec![(AttrId::new("v"), Value::Int(2))],
        "re-assert after retract wins by tx_time"
    );
}
