use crate::build::*;
use crate::{
    ActionOrigin, MemoryPolicyAudit, PolicyAuditSink, PolicyGateway, PolicyLimits,
    PolicyRequestContext,
};
use libremetaverse_types::{
    UUID,
    compat::{CancellationToken, CancellationTokenSource},
};
use std::sync::{Arc, Mutex};

#[derive(Default)]
struct FakeGrid {
    calls: Mutex<Vec<String>>,
    fail: Mutex<Vec<String>>,
    deny_land: Mutex<bool>,
    owned: Mutex<bool>,
    next: Mutex<u32>,
    hang_create: Mutex<bool>,
}
impl FakeGrid {
    fn calls(&self) -> Vec<String> {
        self.calls.lock().unwrap().clone()
    }
    fn record(&self, value: &str) -> Result<(), BuildError> {
        self.calls.lock().unwrap().push(value.to_owned());
        if self
            .fail
            .lock()
            .unwrap()
            .iter()
            .any(|failed| failed == value)
        {
            Err(BuildError::GridOperation)
        } else {
            Ok(())
        }
    }
}
impl BuildGrid for FakeGrid {
    fn validate_land(&self, _: &str, _: &[[i32; 3]], _: CancellationToken) -> BuildFuture<'_, ()> {
        Box::pin(async move {
            self.record("land")?;
            if *self.deny_land.lock().unwrap() {
                Err(BuildError::LandDenied)
            } else {
                Ok(())
            }
        })
    }
    fn texture_is_owned(&self, _: UUID, _: CancellationToken) -> BuildFuture<'_, bool> {
        Box::pin(async move {
            self.record("texture")?;
            Ok(*self.owned.lock().unwrap())
        })
    }
    fn create_prim(
        &self,
        _: &str,
        prim: &BuildPrim,
        _: CancellationToken,
    ) -> BuildFuture<'_, PrimReceipt> {
        let id = prim.id.clone();
        Box::pin(async move {
            self.record(&format!("create:{id}"))?;
            if *self.hang_create.lock().unwrap() {
                std::future::pending::<()>().await;
            }
            let mut next = self.next.lock().unwrap();
            *next += 1;
            Ok(PrimReceipt {
                plan_id: id,
                object_id: UUID::new_with_string(format!("00000000-0000-0000-0000-{:012}", *next))
                    .unwrap(),
                local_id: *next,
            })
        })
    }
    fn configure_prim(
        &self,
        _: &PrimReceipt,
        prim: &BuildPrim,
        _: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let id = prim.id.clone();
        Box::pin(async move { self.record(&format!("configure:{id}")) })
    }
    fn link_prims(
        &self,
        _: &PrimReceipt,
        child: &PrimReceipt,
        _: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let id = child.plan_id.clone();
        Box::pin(async move { self.record(&format!("link:{id}")) })
    }
    fn insert_script(
        &self,
        receipt: &PrimReceipt,
        _: &str,
        _: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let id = receipt.plan_id.clone();
        Box::pin(async move { self.record(&format!("script:{id}")) })
    }
    fn confirm_prim(&self, receipt: &PrimReceipt, _: CancellationToken) -> BuildFuture<'_, ()> {
        let id = receipt.plan_id.clone();
        Box::pin(async move { self.record(&format!("confirm:{id}")) })
    }
    fn delete_owned_prim(
        &self,
        receipt: &PrimReceipt,
        _: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let id = receipt.plan_id.clone();
        Box::pin(async move { self.record(&format!("delete:{id}")) })
    }
}

fn prim(id: &str, parent: Option<&str>) -> BuildPrim {
    BuildPrim {
        id: id.into(),
        parent: parent.map(str::to_owned),
        shape: BuildShape::Box,
        position_millimeters: [1_000, 2_000, 3_000],
        scale_millimeters: [500, 500, 500],
        rotation_degrees: [0.0, 0.0, 0.0],
        color_rgba: [1.0, 0.5, 0.0, 1.0],
        material: "wood".into(),
        texture_inventory_id: None,
        name: id.into(),
        description: "safe".into(),
        script: None,
    }
}
fn plan() -> BuildPlan {
    BuildPlan {
        version: 1,
        region_id: "region-a".into(),
        prims: vec![prim("root", None), prim("child", Some("root"))],
    }
}

#[test]
fn validation_rejects_adversarial_and_unbounded_plans() {
    let grid = Arc::new(FakeGrid::default());
    let service = BuildService::new(grid, BuildLimits::default()).unwrap();
    let mut cases = Vec::new();
    let mut p = plan();
    p.version = 2;
    cases.push(p);
    let mut p = plan();
    p.prims[1].parent = Some("child".into());
    cases.push(p);
    let mut p = plan();
    p.prims[0].scale_millimeters[0] = 0;
    cases.push(p);
    let mut p = plan();
    p.prims[0].rotation_degrees[0] = f64::NAN;
    cases.push(p);
    let mut p = plan();
    p.prims[0].position_millimeters[0] = 99_000;
    cases.push(p);
    let mut p = plan();
    p.prims[0].material = "money".into();
    cases.push(p);
    let mut p = plan();
    p.prims[0].texture_inventory_id = Some("https://evil.invalid/a".into());
    cases.push(p);
    let mut p = plan();
    p.prims[0].script = Some(
        "default { state_entry() { llHTTPRequest(\"https://evil.invalid\", [], \"\"); } }".into(),
    );
    cases.push(p);
    assert!(cases.iter().all(|p| service.validate(p).is_err()));
}

#[tokio::test]
async fn dry_run_checks_rights_and_owned_assets_without_mutation() {
    let grid = Arc::new(FakeGrid::default());
    *grid.owned.lock().unwrap() = true;
    let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
    let mut p = plan();
    p.prims[0].texture_inventory_id = Some("00000000-0000-0000-0000-000000000099".into());
    let report = service
        .dry_run(&p, CancellationToken::default())
        .await
        .unwrap();
    assert_eq!(report.prim_count, 2);
    assert_eq!(report.link_count, 1);
    assert_eq!(grid.calls(), vec!["land", "texture"]);
}

#[tokio::test]
async fn land_denial_and_cancellation_stop_before_mutation() {
    let grid = Arc::new(FakeGrid::default());
    *grid.deny_land.lock().unwrap() = true;
    let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
    assert_eq!(
        service
            .dry_run(&plan(), CancellationToken::default())
            .await
            .unwrap_err(),
        BuildError::LandDenied
    );
    assert!(!grid.calls().iter().any(|call| call.starts_with("create:")));
    let cancelled = CancellationTokenSource::new();
    cancelled.cancel();
    assert_eq!(
        service
            .execute("tx-cancel", "call", plan(), cancelled.token())
            .await
            .unwrap_err(),
        BuildError::Cancelled
    );
}

#[tokio::test]
async fn partial_create_reply_times_out_without_guessing_an_object_identity() {
    let grid = Arc::new(FakeGrid::default());
    *grid.hang_create.lock().unwrap() = true;
    let limits = BuildLimits {
        step_timeout: std::time::Duration::from_millis(1),
        ..BuildLimits::default()
    };
    let service = BuildService::new(grid, limits).unwrap();
    assert_eq!(
        service
            .execute("tx-timeout", "call", plan(), CancellationToken::default())
            .await
            .unwrap_err(),
        BuildError::TimedOut
    );
    assert!(service.orphan_ids("tx-timeout").is_empty());
}

#[tokio::test]
async fn representative_build_confirms_configures_scripts_links_and_identity() {
    let grid = Arc::new(FakeGrid::default());
    let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
    let mut p = plan();
    p.prims[1].script = Some("default { state_entry() { llOwnerSay(\"ready\"); } }".into());
    let receipt = service
        .execute("tx-1", "call-1", p, CancellationToken::default())
        .await
        .unwrap();
    assert_eq!(receipt.object_ids.len(), 2);
    assert_eq!(
        receipt.root_object_id,
        "00000000-0000-0000-0000-000000000001"
    );
    assert_eq!(
        grid.calls(),
        vec![
            "land",
            "create:root",
            "confirm:root",
            "configure:root",
            "confirm:root",
            "create:child",
            "confirm:child",
            "configure:child",
            "confirm:child",
            "script:child",
            "confirm:child",
            "link:child",
            "confirm:child"
        ]
    );
    assert_eq!(
        service.observations().last().unwrap().kind,
        BuildObservationKind::Completed
    );
    let cleanup = service
        .cleanup_completed("tx-1", "call-2", CancellationToken::default())
        .await
        .unwrap();
    assert_eq!(cleanup.deleted_object_ids.len(), 2);
    assert!(cleanup.orphan_ids.is_empty());
    assert_eq!(
        &grid.calls()[grid.calls().len() - 2..],
        ["delete:child", "delete:root"]
    );
    assert_eq!(
        service
            .cleanup_completed("tx-1", "call-3", CancellationToken::default())
            .await
            .unwrap_err(),
        BuildError::UnknownTransaction
    );
}

#[tokio::test]
async fn failure_cleans_only_objects_created_by_transaction_in_reverse_order() {
    let grid = Arc::new(FakeGrid::default());
    *grid.fail.lock().unwrap() = vec!["link:child".into()];
    let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
    assert_eq!(
        service
            .execute("tx-2", "call-2", plan(), CancellationToken::default())
            .await
            .unwrap_err(),
        BuildError::GridOperation
    );
    let calls = grid.calls();
    assert_eq!(&calls[calls.len() - 2..], ["delete:child", "delete:root"]);
    assert!(service.orphan_ids("tx-2").is_empty());
}

#[tokio::test]
async fn cleanup_failure_records_recoverable_orphan_identity() {
    let grid = Arc::new(FakeGrid::default());
    *grid.fail.lock().unwrap() = vec!["configure:root".into(), "delete:root".into()];
    let service = BuildService::new(grid.clone(), BuildLimits::default()).unwrap();
    assert_eq!(
        service
            .execute("tx-3", "call-3", plan(), CancellationToken::default())
            .await
            .unwrap_err(),
        BuildError::CleanupIncomplete
    );
    assert_eq!(service.orphan_ids("tx-3").len(), 1);
    assert!(
        service
            .observations()
            .iter()
            .any(|o| o.kind == BuildObservationKind::Orphaned)
    );
}

#[test]
fn policy_requires_bound_operator_approval_only_above_small_limit() {
    let tools = build_policy_tools(BuildLimits::default()).unwrap();
    assert_eq!(tools.len(), 3);
    assert_eq!(tools[0].definition.name.as_str(), BUILD_DRY_RUN_TOOL);
    assert!(!tools[0].definition.mutating);
    assert_eq!(tools[1].definition.name.as_str(), BUILD_EXECUTE_TOOL);
    assert!(tools[1].definition.mutating);
    assert!(matches!(
        tools[1].approval,
        crate::ApprovalRule::WhenExceeds(_)
    ));
    assert_eq!(tools[2].definition.name.as_str(), BUILD_CLEANUP_TOOL);
    assert!(tools[2].definition.mutating);
    assert!(matches!(tools[2].approval, crate::ApprovalRule::Never));
    tools[1]
        .definition
        .schema
        .validate_value(&serde_json::json!({
            "plan": {
                "version": 1,
                "region_id": "region-a",
                "prims": [{
                    "id": "root",
                    "parent": null,
                    "shape": "box",
                    "position_millimeters": [1, 2, 3],
                    "scale_millimeters": [500, 500, 500],
                    "rotation_degrees": [0, 0, 0],
                    "color_rgba": [1, 0.5, 0, 1],
                    "material": "wood",
                    "texture_inventory_id": null,
                    "name": "root",
                    "description": "safe",
                    "script": null
                }]
            }
        }))
        .unwrap();
    let service = BuildService::new(Arc::new(FakeGrid::default()), BuildLimits::default()).unwrap();
    assert!(!service.validate(&plan()).unwrap().approval_required);
    let mut larger = plan();
    for index in 2..5 {
        larger
            .prims
            .push(prim(&format!("child-{index}"), Some("root")));
    }
    assert!(service.validate(&larger).unwrap().approval_required);
}

#[test]
fn build_tools_are_invisible_to_public_and_unauthorized_origins() {
    let authorized = UUID::new_with_string("11111111-1111-4111-8111-111111111111".into()).unwrap();
    let stranger = UUID::new_with_string("22222222-2222-4222-8222-222222222222".into()).unwrap();
    let audit = Arc::new(MemoryPolicyAudit::new(32).unwrap());
    let sink: Arc<dyn PolicyAuditSink> = audit;
    let gateway = PolicyGateway::new(
        std::collections::BTreeSet::from([authorized]),
        build_policy_tools(BuildLimits::default()).unwrap(),
        PolicyLimits::default(),
        sink,
    )
    .unwrap();
    let public =
        PolicyRequestContext::new(ActionOrigin::public_chat(authorized), "public", "call").unwrap();
    let unauthorized =
        PolicyRequestContext::new(ActionOrigin::instant_message(stranger), "im", "call").unwrap();
    let authorized_im = PolicyRequestContext::new(
        ActionOrigin::instant_message(authorized),
        "authorized",
        "call",
    )
    .unwrap();
    assert!(gateway.tools_for(&public, 1).is_empty());
    assert!(gateway.tools_for(&unauthorized, 1).is_empty());
    assert_eq!(gateway.tools_for(&authorized_im, 1).len(), 3);
}
