use crate::vision::*;
use crate::{
    BoundedText, Capability, CompletionMessage, InteractionChannel, InteractionIntent,
    InteractionModelError, InteractionOrigin, InteractionResponder, MessageRole, ResponderFuture,
    ResponseRequest, VisibleResponse,
};
use libremetaverse_types::{
    UUID,
    compat::{CancellationToken, CancellationTokenSource},
};
use std::sync::{Arc, Mutex};
use std::time::Duration;

struct FakeScene {
    scene: SceneSnapshot,
    pending: Mutex<bool>,
}
impl SceneSource for FakeScene {
    fn capture_scene(
        &self,
        _: u64,
        cancellation: CancellationToken,
    ) -> VisionFuture<'_, SceneSnapshot> {
        Box::pin(async move {
            if *self.pending.lock().unwrap() {
                tokio::select! {()=cancellation.cancelled()=>Err(VisionError::Cancelled),()=std::future::pending()=>unreachable!()}
            } else {
                Ok(self.scene.clone())
            }
        })
    }
}
fn uuid(value: u32) -> UUID {
    UUID::new_with_string(format!("00000000-0000-4000-8000-{value:012}")).unwrap()
}

#[cfg(feature = "live-grid")]
#[tokio::test]
async fn background_prefetch_is_idempotent_and_stoppable_without_a_simulator() {
    let owner = crate::backend::LibremetaverseClientOwner::new().unwrap();
    let source = Arc::new(LibremetaverseSceneSource::new(
        &owner,
        VisionLimits::default(),
    ));
    source.start_prefetch();
    source.start_prefetch();
    tokio::task::yield_now().await;
    source.stop_prefetch().await;
    source.stop_prefetch().await;
    assert_eq!(source.prefetch_passes(), 0);
}

fn triangle(z: f32, color: [u8; 4]) -> SceneTriangle {
    SceneTriangle {
        vertices: [[-1.0, -1.0, z], [1.0, -1.0, z], [0.0, 1.0, z]],
        colors_srgb: [color; 3],
    }
}

#[cfg(feature = "live-grid")]
#[test]
fn extended_terrain_layout_uses_received_patch_coordinates() {
    let mut terrain = (0..65_536)
        .map(|_| libremetaverse::TerrainPatch {
            data: Vec::new(),
            x: 0,
            y: 0,
        })
        .collect::<Vec<_>>();
    terrain[33 * 256 + 42] = libremetaverse::TerrainPatch {
        data: vec![20.0; 256],
        x: 42,
        y: 33,
    };
    assert_eq!(terrain_layout(&terrain, (256.0, 256.0)), (688, 544, 256));
    assert_eq!(terrain_height(&terrain, 256, 675, 538), Some(20.0));
}

#[cfg(feature = "live-grid")]
#[test]
fn terrain_uses_corner_bands_and_only_builds_the_camera_neighborhood() {
    assert!((bilinear_corners([0.0, 10.0, 20.0, 30.0], 0.5, 0.5) - 15.0).abs() < f32::EPSILON);
    assert_eq!(
        blend_color([0, 20, 40, 255], [100, 120, 140, 255], 0.5),
        [50, 70, 90, 255]
    );
    let terrain = (0..16)
        .flat_map(|y| {
            (0..16).map(move |x| libremetaverse::TerrainPatch {
                data: vec![20.0; 256],
                x,
                y,
            })
        })
        .collect::<Vec<_>>();
    let triangles = terrain_triangles(
        &terrain,
        (256.0, 256.0),
        19.0,
        &std::collections::BTreeMap::new(),
        TerrainMaterials {
            texture_ids: [UUID::zero(); 4],
            start_heights: [0.0; 4],
            height_ranges: [30.0; 4],
        },
        [128.0, 128.0, 22.0],
        8.0,
        10_000,
    );
    assert_eq!(triangles.len(), 512);
    assert!(
        triangles.iter().all(|triangle| triangle
            .vertices
            .iter()
            .all(|vertex| (120.0..=136.0).contains(&vertex[0])
                && (120.0..=136.0).contains(&vertex[1])))
    );
}

#[cfg(feature = "live-grid")]
#[test]
fn scene_meshes_use_visual_lods_at_the_default_view_distance() {
    let mut prim = libremetaverse::Primitive::new_with_constructor().unwrap();
    prim.position = libremetaverse_types::Vector3 {
        x: 31.0,
        y: 0.0,
        z: 0.0,
    };
    assert_eq!(
        scene_detail_level(&prim, [0.0; 3]),
        libremetaverse::rendering::DetailLevel::Highest
    );
    prim.position.x = 64.0;
    assert_eq!(
        scene_detail_level(&prim, [0.0; 3]),
        libremetaverse::rendering::DetailLevel::High
    );
}

#[cfg(feature = "live-grid")]
#[test]
fn oversized_scene_textures_are_mip_sized_without_distortion() {
    let mut image = libremetaverse_imaging::ManagedImage::new(
        4,
        2,
        libremetaverse_imaging::ManagedImageImageChannels::COLOR,
    )
    .unwrap();
    fit_texture_for_view(&mut image, 2).unwrap();
    assert_eq!((image.width, image.height), (2, 1));
    assert_eq!(image.red.len(), 2);

    let mut cutout = libremetaverse_imaging::ManagedImage::new(
        4,
        2,
        libremetaverse_imaging::ManagedImageImageChannels::COLOR
            | libremetaverse_imaging::ManagedImageImageChannels::ALPHA,
    )
    .unwrap();
    cutout
        .red
        .copy_from_slice(&[255, 255, 0, 0, 255, 255, 0, 0]);
    cutout
        .green
        .copy_from_slice(&[220, 220, 0, 0, 220, 220, 0, 0]);
    cutout
        .alpha
        .copy_from_slice(&[255, 255, 0, 0, 255, 255, 0, 0]);
    fit_texture_for_view(&mut cutout, 3).unwrap();
    assert_eq!((cutout.width, cutout.height), (3, 1));
    assert_eq!((cutout.red[1], cutout.green[1]), (255, 219));
    assert_eq!(cutout.alpha[1], 128);
}

#[cfg(feature = "live-grid")]
#[test]
fn renderer_uses_the_camera_axis_that_points_at_the_requested_target() {
    let camera = libremetaverse::AgentManagerAgentMovementAgentCamera::new().unwrap();
    camera
        .look_at_with_vector3_vector3(
            libremetaverse_types::Vector3::zero(),
            libremetaverse_types::Vector3::unit_y(),
        )
        .unwrap();
    assert_eq!(
        native_camera_forward(&camera),
        libremetaverse_types::Vector3::unit_y()
    );
}

#[cfg(feature = "live-grid")]
#[test]
fn scene_faces_are_shaded_without_changing_alpha() {
    let shade = triangle_shade([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
    let color = shade_color([200, 160, 120, 77], shade);
    assert!(color[0] < 200);
    assert_eq!(color[3], 77);
}

#[cfg(feature = "live-grid")]
#[test]
fn legacy_faces_preserve_face_alpha_without_inventing_specular_light() {
    let mut face = libremetaverse::PrimitiveTextureEntryFace::default();
    face.set_rgba(libremetaverse_types::Color4 {
        r: 0.25,
        g: 0.5,
        b: 0.75,
        a: 0.0,
    });
    let material = face_material(
        &face,
        &std::collections::BTreeMap::new(),
        &std::collections::BTreeMap::new(),
        &std::collections::BTreeMap::new(),
        true,
    );
    let metacrate_rendering_wgpu::Material::BlinnPhong(material) = material else {
        panic!("legacy face did not produce a Blinn-Phong material");
    };
    assert_eq!(material.diffuse_color_srgb, [64, 128, 191, 0]);
    assert_eq!(material.specular_color_srgb, [0; 3]);
    assert_eq!(
        material.alpha_mode,
        metacrate_rendering_wgpu::AlphaMode::Blend
    );
    assert_eq!(material.alpha_cutoff, Some(0.5));
}

#[cfg(feature = "live-grid")]
#[test]
fn face_alpha_overrides_an_opaque_material_mode() {
    let mut face = libremetaverse::PrimitiveTextureEntryFace::default();
    face.set_rgba(libremetaverse_types::Color4 {
        r: 1.0,
        g: 1.0,
        b: 1.0,
        a: 0.0,
    });
    let mut legacy = libremetaverse::materials::LegacyMaterial::new_with_constructor().unwrap();
    legacy.set_id(face.material_id());
    legacy.set_diffuse_alpha_mode(libremetaverse::materials::LegacyMaterialAlphaMode::None);
    let legacy = [(legacy.id(), legacy)].into_iter().collect();
    let material = face_material(
        &face,
        &std::collections::BTreeMap::new(),
        &legacy,
        &std::collections::BTreeMap::new(),
        false,
    );
    assert_eq!(
        material.alpha_mode(),
        metacrate_rendering_wgpu::AlphaMode::Blend
    );

    face.set_rgba(libremetaverse_types::Color4::white());
    let material = face_material(
        &face,
        &std::collections::BTreeMap::new(),
        &legacy,
        &std::collections::BTreeMap::new(),
        true,
    );
    assert_eq!(
        material.alpha_mode(),
        metacrate_rendering_wgpu::AlphaMode::Mask { cutoff: 0.5 }
    );
    let metacrate_rendering_wgpu::Material::BlinnPhong(material) = material else {
        panic!("legacy face did not produce a Blinn-Phong material");
    };
    assert_eq!(material.alpha_cutoff, Some(0.5));

    face.set_rgba(libremetaverse_types::Color4 {
        a: 0.5,
        ..libremetaverse_types::Color4::white()
    });
    let mut mask = libremetaverse::materials::LegacyMaterial::new_with_constructor().unwrap();
    mask.set_id(face.material_id());
    mask.set_diffuse_alpha_mode(libremetaverse::materials::LegacyMaterialAlphaMode::Mask);
    mask.set_alpha_mask_cutoff(128);
    let masks = [(mask.id(), mask)].into_iter().collect();
    let material = face_material(
        &face,
        &std::collections::BTreeMap::new(),
        &masks,
        &std::collections::BTreeMap::new(),
        true,
    );
    let metacrate_rendering_wgpu::Material::BlinnPhong(material) = material else {
        panic!("legacy face did not produce a Blinn-Phong material");
    };
    assert_eq!(
        material.alpha_mode,
        metacrate_rendering_wgpu::AlphaMode::Blend
    );
    assert_eq!(material.alpha_cutoff, Some(128.0 / 255.0));
}

#[cfg(feature = "live-grid")]
#[test]
fn scene_visibility_uses_configured_distance_and_avatar_view() {
    assert!(scene_within_distance(
        libremetaverse_types::Vector3 {
            x: -4.0,
            y: 0.0,
            z: 0.0
        },
        [0.0; 3],
        64.0,
    ));
    assert!(!scene_within_distance(
        libremetaverse_types::Vector3 {
            x: 65.0,
            y: 0.0,
            z: 0.0
        },
        [0.0; 3],
        64.0,
    ));
    assert!(scene_center_visible(
        libremetaverse_types::Vector3 {
            x: 4.0,
            y: 0.0,
            z: 0.0
        },
        [0.0; 3],
        [1.0, 0.0, 0.0],
        60_f32.to_radians(),
        16.0 / 9.0,
        64.0,
    ));
    assert!(!scene_center_visible(
        libremetaverse_types::Vector3 {
            x: -4.0,
            y: 0.0,
            z: 0.0
        },
        [0.0; 3],
        [1.0, 0.0, 0.0],
        60_f32.to_radians(),
        16.0 / 9.0,
        64.0,
    ));
    assert!(scene_center_focused(
        libremetaverse_types::Vector3 {
            x: 4.0,
            y: 0.0,
            z: 0.0
        },
        [0.0; 3],
        [1.0, 0.0, 0.0],
        64.0,
    ));
    assert!(!scene_center_focused(
        libremetaverse_types::Vector3 {
            x: 4.0,
            y: 4.0,
            z: 0.0
        },
        [0.0; 3],
        [1.0, 0.0, 0.0],
        64.0,
    ));
}
fn scene() -> SceneSnapshot {
    SceneSnapshot {
        generation: 7,
        scene_sequence: 1,
        observed_unix_millis: 1_700_000_000_000,
        region_id: uuid(1),
        region_name: "Fixture Region".into(),
        camera: CameraPose {
            position: [0.0, 0.0, 0.0],
            forward: [0.0, 0.0, 1.0],
            up: [0.0, 1.0, 0.0],
            vertical_fov_degrees: 60.0,
        },
        entities: vec![
            SceneEntity {
                id: uuid(3),
                kind: SceneEntityKind::Resident,
                display_name: "Private Resident".into(),
                triangles: vec![triangle(4.0, [255, 0, 0, 255])],
                renderables: Vec::new(),
                texture_available: false,
            },
            SceneEntity {
                id: uuid(2),
                kind: SceneEntityKind::Object,
                display_name: "Near".into(),
                triangles: vec![triangle(2.0, [0, 255, 0, 255])],
                renderables: Vec::new(),
                texture_available: true,
            },
        ]
        .into(),
        textures: Vec::new().into(),
        completeness: SnapshotCompleteness {
            textures_missing: 1,
            terrain_available: false,
            ..SnapshotCompleteness::default()
        },
        texture_fetches: 1,
        texture_bytes: 128,
        decoded_texture_pixels: 16,
        timings: SceneBuildTimings::default(),
    }
}

#[tokio::test]
async fn golden_scene_is_deterministic_depth_ordered_and_privacy_marked() {
    let source = Arc::new(FakeScene {
        scene: scene(),
        pending: Mutex::new(false),
    });
    let service = VisionService::new(
        source,
        VisionLimits {
            width: 64,
            height: 64,
            minimum_interval: std::time::Duration::ZERO,
            ..VisionLimits::default()
        },
    )
    .unwrap();
    service.set_generation(7);
    let first = service
        .capture("one", 7, CancellationToken::default())
        .await
        .unwrap();
    let second = service
        .capture("two", 7, CancellationToken::default())
        .await
        .unwrap();
    assert_eq!(first.jpeg, second.jpeg);
    assert_eq!(first.image_sha256, second.image_sha256);
    assert_eq!(
        first.image_sha256,
        "63a9e3502b7ca43ac8b3560f516fcf1fbed96f74f86e9818d7d88db2cc39555b"
    );
    assert!(first.summary.contains("privacy-marked residents"));
    assert!(!first.summary.contains("Private Resident"));
    assert!(first.summary.contains("textures_missing=1"));
    assert!(first.data_url.starts_with("data:image/jpeg;base64,"));
    let mut decoder = jpeg_decoder::Decoder::new(first.jpeg.as_slice());
    let pixels = decoder.decode().unwrap();
    let info = decoder.info().unwrap();
    assert_eq!((info.width, info.height), (64, 64));
    let center = ((info.height as usize / 2) * info.width as usize + info.width as usize / 2) * 3;
    assert!(pixels[center + 1] > 180 && pixels[center] < 80 && pixels[center + 2] < 80);
}

#[cfg(feature = "live-grid")]
#[tokio::test(flavor = "multi_thread")]
async fn offscreen_wgpu_renders_depth_ordered_scene_when_an_adapter_is_available() {
    let Ok(renderer) = metacrate_game_loop::ViewportLoop::start(Duration::from_millis(100)) else {
        return;
    };
    let limits = VisionLimits {
        width: 64,
        height: 64,
        minimum_interval: std::time::Duration::ZERO,
        ..VisionLimits::default()
    };
    let scene = scene();
    let triangles = scene
        .entities
        .iter()
        .flat_map(|entity| entity.triangles.iter().cloned())
        .collect::<Vec<_>>();
    let renderables = scene_renderable_from_triangles(&triangles)
        .into_iter()
        .collect::<Vec<_>>();
    renderer.submit(metacrate_game_loop::ViewportScene {
        sequence: scene.scene_sequence,
        camera: scene.camera,
        renderables: renderables.into(),
        textures: Arc::clone(&scene.textures),
        background_srgb: [72, 96, 120, 255],
        limits: metacrate_rendering_wgpu::RenderLimits {
            width: limits.width,
            height: limits.height,
            max_triangles: limits.max_triangles,
            max_texture_bytes: limits.max_texture_bytes,
            max_texture_pixels: limits.max_decode_pixels,
            far_distance: 512,
        },
    });
    tokio::time::timeout(Duration::from_secs(3), async {
        while renderer.stats().completed_frames < 2 {
            tokio::time::sleep(Duration::from_millis(20)).await;
        }
    })
    .await
    .expect("fixed-rate viewport did not publish repeated frames");
    let rgba = renderer.latest().unwrap().value.rgba.clone();
    assert_eq!(renderer.stats().render_errors, 0);
    assert_eq!(rgba.len(), 64 * 64 * 4);
    let center = (32 * 64 + 32) * 4;
    assert!(rgba[center + 1] > 180 && rgba[center] < 80 && rgba[center + 2] < 80);
}

#[tokio::test]
async fn invalid_camera_huge_scene_stale_generation_and_size_limit_fail_closed() {
    let mut bad = scene();
    bad.camera.forward = [0.0, 0.0, 0.0];
    let source = Arc::new(FakeScene {
        scene: bad,
        pending: Mutex::new(false),
    });
    let service = VisionService::new(
        source,
        VisionLimits {
            width: 64,
            height: 64,
            minimum_interval: std::time::Duration::ZERO,
            ..VisionLimits::default()
        },
    )
    .unwrap();
    service.set_generation(7);
    assert_eq!(
        service
            .capture("bad", 7, CancellationToken::default())
            .await
            .unwrap_err(),
        VisionError::InvalidScene
    );
    assert_eq!(
        service
            .capture("stale", 6, CancellationToken::default())
            .await
            .unwrap_err(),
        VisionError::StaleGeneration
    );
    let source = Arc::new(FakeScene {
        scene: scene(),
        pending: Mutex::new(false),
    });
    let service = VisionService::new(
        source,
        VisionLimits {
            width: 64,
            height: 64,
            max_jpeg_bytes: 4096,
            minimum_interval: std::time::Duration::ZERO,
            ..VisionLimits::default()
        },
    )
    .unwrap();
    service.set_generation(7);
    let result = service
        .capture("size", 7, CancellationToken::default())
        .await;
    assert!(matches!(result, Ok(_) | Err(VisionError::ResourceLimit)));

    let mut excessive_texture_work = scene();
    excessive_texture_work.texture_fetches = VisionLimits::default().max_texture_fetches + 1;
    let source = Arc::new(FakeScene {
        scene: excessive_texture_work,
        pending: Mutex::new(false),
    });
    let service = VisionService::new(
        source,
        VisionLimits {
            width: 64,
            height: 64,
            minimum_interval: std::time::Duration::ZERO,
            ..VisionLimits::default()
        },
    )
    .unwrap();
    service.set_generation(7);
    assert_eq!(
        service
            .capture("texture-budget", 7, CancellationToken::default())
            .await
            .unwrap_err(),
        VisionError::ResourceLimit
    );
}

#[tokio::test]
async fn cancellation_and_region_change_interrupt_nonblocking_capture() {
    let source = Arc::new(FakeScene {
        scene: scene(),
        pending: Mutex::new(true),
    });
    let service = Arc::new(
        VisionService::new(
            source,
            VisionLimits {
                width: 64,
                height: 64,
                minimum_interval: std::time::Duration::ZERO,
                ..VisionLimits::default()
            },
        )
        .unwrap(),
    );
    service.set_generation(7);
    let cancellation = CancellationTokenSource::new();
    cancellation.cancel();
    assert_eq!(
        service
            .capture("cancel", 7, cancellation.token())
            .await
            .unwrap_err(),
        VisionError::Cancelled
    );
    let service2 = service.clone();
    let task = tokio::spawn(async move {
        service2
            .capture("region", 7, CancellationToken::default())
            .await
    });
    tokio::task::yield_now().await;
    service.set_generation(8);
    assert!(matches!(
        task.await.unwrap(),
        Err(VisionError::Superseded | VisionError::Cancelled)
    ));

    service.set_generation(9);
    let first_service = service.clone();
    let first = tokio::spawn(async move {
        first_service
            .capture("first", 9, CancellationToken::default())
            .await
    });
    tokio::task::yield_now().await;
    let second_cancel = CancellationTokenSource::new();
    let second_service = service.clone();
    let second_token = second_cancel.token();
    let second =
        tokio::spawn(async move { second_service.capture("second", 9, second_token).await });
    assert_eq!(
        tokio::time::timeout(std::time::Duration::from_secs(1), first)
            .await
            .unwrap()
            .unwrap()
            .unwrap_err(),
        VisionError::Superseded
    );
    second_cancel.cancel();
    assert_eq!(second.await.unwrap().unwrap_err(), VisionError::Cancelled);
}

#[test]
fn explicit_visual_intent_is_narrow() {
    assert!(visual_question("What do you see in front of you?"));
    assert!(visual_question("Please look at this viewport"));
    assert!(!visual_question("hello there"));
    assert!(!visual_question("build a cube"));
}

struct FakeResponder {
    parts: Mutex<usize>,
    unsupported: bool,
}
impl InteractionResponder for FakeResponder {
    fn respond(&self, request: ResponseRequest, _: CancellationToken) -> ResponderFuture<'_> {
        Box::pin(async move {
            *self.parts.lock().unwrap() = request.messages.last().unwrap().content.len();
            if self.unsupported {
                Err(InteractionModelError::MultimodalUnsupported)
            } else {
                VisibleResponse::new("seen").map_err(|_| InteractionModelError::Failed)
            }
        })
    }
}
fn request(text: &str) -> ResponseRequest {
    ResponseRequest {
        delivery_id: BoundedText::new("id", "delivery").unwrap(),
        sender_id: uuid(9),
        session_id: BoundedText::new("session", "session").unwrap(),
        channel: InteractionChannel::DirectIm,
        origin: InteractionOrigin::AuthorizedIm,
        intent: InteractionIntent::Informational,
        capabilities: std::collections::BTreeSet::from([Capability::Informational]),
        messages: vec![CompletionMessage::text(MessageRole::Avatar, text).unwrap()],
    }
}

#[tokio::test]
async fn responder_adds_generic_image_payload_and_returns_summary_on_capability_rejection() {
    let source = Arc::new(FakeScene {
        scene: scene(),
        pending: Mutex::new(false),
    });
    let vision = Arc::new(
        VisionService::new(
            source,
            VisionLimits {
                width: 64,
                height: 64,
                minimum_interval: std::time::Duration::ZERO,
                ..VisionLimits::default()
            },
        )
        .unwrap(),
    );
    vision.set_generation(7);
    let endpoint = Arc::new(FakeResponder {
        parts: Mutex::new(0),
        unsupported: true,
    });
    let responder = VisionAugmentedResponder::new(vision, endpoint.clone());
    let response = responder
        .respond(request("What do you see?"), CancellationToken::default())
        .await
        .unwrap();
    assert!(response.as_str().contains("does not support visual input"));
    assert!(response.as_str().contains("Reconstructed viewport"));
    assert_eq!(*endpoint.parts.lock().unwrap(), 3);
}

#[tokio::test]
async fn responder_never_captures_for_ordinary_text() {
    let source = Arc::new(FakeScene {
        scene: scene(),
        pending: Mutex::new(false),
    });
    let vision = Arc::new(
        VisionService::new(
            source,
            VisionLimits {
                width: 64,
                height: 64,
                minimum_interval: std::time::Duration::ZERO,
                ..VisionLimits::default()
            },
        )
        .unwrap(),
    );
    vision.set_generation(7);
    let endpoint = Arc::new(FakeResponder {
        parts: Mutex::new(0),
        unsupported: false,
    });
    let responder = VisionAugmentedResponder::new(vision.clone(), endpoint.clone());
    assert_eq!(
        responder
            .respond(request("hello"), CancellationToken::default())
            .await
            .unwrap()
            .as_str(),
        "seen"
    );
    assert_eq!(*endpoint.parts.lock().unwrap(), 1);
    assert!(vision.observations().is_empty());
}
