use rustix::event::{PollFd, PollFlags, poll};
use rustix::process::{Pid, Signal, WaitOptions};
use std::collections::HashMap;
use std::io::Write;
use std::mem::ManuallyDrop;
use std::os::fd::{AsRawFd, BorrowedFd, OwnedFd};
use std::os::unix::net::UnixStream;
use std::sync::{
    Arc, Mutex, Once,
    atomic::{AtomicBool, Ordering},
};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use wayland_protocols::xdg::{
    decoration::zv1::server::zxdg_toplevel_decoration_v1, shell::server::xdg_toplevel,
};
use wayland_server::Resource;
use wayland_server::protocol::{wl_output, wl_pointer};
use xcb::{Xid, x};
use xwayland_satellite as xwls;
use xwayland_satellite::xstate::{MoveResizeDirection, WmSizeHintsFlags, WmState};
use xwls::timespec_from_millis;

#[derive(Default)]
struct TestDataInner {
    server_created: AtomicBool,
    server_connected: AtomicBool,
    display: Mutex<Option<String>>,
    server: Mutex<Option<UnixStream>>,
    pid: Mutex<Option<u32>>,
    quit_rx: Mutex<Option<UnixStream>>,
}

#[derive(Default, Clone)]
struct TestData(Arc<TestDataInner>);

impl TestData {
    fn new(server: UnixStream, quit_rx: UnixStream) -> Self {
        Self(Arc::new(TestDataInner {
            server: Mutex::new(server.into()),
            quit_rx: Mutex::new(Some(quit_rx)),
            ..Default::default()
        }))
    }
}

impl std::ops::Deref for TestData {
    type Target = Arc<TestDataInner>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl xwls::RunData for TestData {
    fn created_server(&self) {
        self.server_created.store(true, Ordering::Relaxed);
    }

    fn connected_server(&self) {
        self.server_connected.store(true, Ordering::Relaxed);
    }

    fn quit_rx(&self) -> Option<UnixStream> {
        self.quit_rx.lock().unwrap().take()
    }

    fn xwayland_ready(&self, display: String, pid: u32) {
        *self.display.lock().unwrap() = Some(display);
        *self.pid.lock().unwrap() = Some(pid);
    }

    fn display(&self) -> Option<&str> {
        None
    }

    fn listenfds(&mut self) -> Vec<OwnedFd> {
        Vec::new()
    }

    fn server(&self) -> Option<UnixStream> {
        let mut server = self.server.lock().unwrap();
        assert!(server.is_some());
        server.take()
    }

    fn max_req_len_bytes(&self) -> Option<usize> {
        Some(500)
    }
}

struct Fixture {
    testwl: testwl::Server,
    thread: ManuallyDrop<JoinHandle<Option<()>>>,
    pollfd: PollFd<'static>,
    display: String,
    pid: Pid,
    quit_tx: UnixStream,
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let thread = unsafe { ManuallyDrop::take(&mut self.thread) };
        // Sending anything to the quit receiver to stop the main loop. Then we guarantee a main
        // thread does not use file descriptors which outlive the Fixture's BorrowedFd
        // If the receiver end of the pipe closed, the main thread dropped it, which means that
        // thread already terminated
        if self
            .quit_tx
            .write_all(&0_i32.to_ne_bytes())
            .is_err_and(|e| e.kind() != std::io::ErrorKind::BrokenPipe)
        {
            panic!("could not message the main thread to terminate");
        }
        if thread.join().is_err() {
            log::error!("main thread panicked");
        }
        if rustix::process::test_kill_process(self.pid).is_ok() {
            rustix::process::kill_process(self.pid, Signal::TERM).unwrap();
            rustix::process::waitpid(Some(self.pid), WaitOptions::NOHANG).unwrap();
        }
    }
}

impl Fixture {
    fn new_preset(pre_connect: impl FnOnce(&mut testwl::Server)) -> Self {
        static INIT: Once = Once::new();
        INIT.call_once(|| {
            pretty_env_logger::formatted_timed_builder()
                .is_test(true)
                .filter_level(log::LevelFilter::Debug)
                .parse_default_env()
                .init();
        });

        let (quit_tx, quit_rx) = UnixStream::pair().unwrap();

        let (a, b) = UnixStream::pair().unwrap();
        let mut testwl = testwl::Server::new(false);
        pre_connect(&mut testwl);
        testwl.connect(a);
        let our_data = TestData::new(b, quit_rx);
        let data = our_data.clone();
        let thread = std::thread::spawn(move || xwls::main(data));

        // wait for connection
        let fd = unsafe { BorrowedFd::borrow_raw(testwl.poll_fd().as_raw_fd()) };
        let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
        let timeout = timespec_from_millis(1000);
        assert!(poll(&mut [pollfd.clone()], Some(&timeout)).unwrap() > 0);
        testwl.dispatch();

        let try_bool_timeout = |b: &AtomicBool| {
            let timeout = Duration::from_secs(1);
            let mut res = b.load(Ordering::Relaxed);
            let start = Instant::now();
            while !res && start.elapsed() < timeout {
                res = b.load(Ordering::Relaxed);
            }

            res
        };
        assert!(
            try_bool_timeout(&our_data.server_created),
            "creating server"
        );
        assert!(
            try_bool_timeout(&our_data.server_connected),
            "connecting to server"
        );

        let mut f = [pollfd.clone()];
        let start = std::time::Instant::now();
        // Give Xwayland time to do its thing

        let mut ready = our_data.display.lock().unwrap().is_some();
        while !ready && start.elapsed() < Duration::from_millis(2000) {
            let timeout = timespec_from_millis(100);
            let n = poll(&mut f, Some(&timeout)).unwrap();
            if n > 0 {
                testwl.dispatch();
            }
            ready = our_data.display.lock().unwrap().is_some();
        }

        assert!(ready, "connecting to xwayland failed");

        let display = our_data.display.lock().unwrap().take().unwrap();
        let pid = our_data.pid.lock().unwrap().take().unwrap();
        Self {
            testwl,
            thread: ManuallyDrop::new(thread),
            pollfd,
            display,
            pid: Pid::from_raw(pid as _).expect("Xwayland PID was invalid?"),
            quit_tx,
        }
    }
    fn new() -> Self {
        Self::new_preset(|_| {})
    }

    #[track_caller]
    fn wait_and_dispatch(&mut self) {
        let mut pollfd = [self.pollfd.clone()];
        self.testwl.dispatch();
        let timeout = timespec_from_millis(50);
        assert!(
            poll(&mut pollfd, Some(&timeout)).unwrap() > 0,
            "Did not receive any events"
        );
        self.pollfd.clear_revents();
        self.testwl.dispatch();

        while poll(&mut pollfd, Some(&timeout)).unwrap() > 0 {
            self.testwl.dispatch();
            self.pollfd.clear_revents();
        }
    }

    fn configure_and_verify_new_toplevel(
        &mut self,
        connection: &mut Connection,
        window: x::Window,
        surface: testwl::SurfaceId,
    ) {
        let data = self.testwl.get_surface_data(surface).unwrap();
        assert!(
            matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))),
            "surface role was wrong: {:?}",
            data.role
        );

        self.testwl
            .configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]);
        self.testwl.focus_toplevel(surface);
        self.wait_and_dispatch();
        let geometry = connection.get_reply(&x::GetGeometry {
            drawable: x::Drawable::Window(window),
        });

        assert_eq!(geometry.x(), 0);
        assert_eq!(geometry.y(), 0);
        assert_eq!(geometry.width(), 100);
        assert_eq!(geometry.height(), 100);
    }

    #[track_caller]
    fn map_as_toplevel(
        &mut self,
        connection: &mut Connection,
        window: x::Window,
    ) -> testwl::SurfaceId {
        connection.map_window(window);
        self.wait_and_dispatch();
        let surface = self
            .testwl
            .last_created_surface_id()
            .expect("No surface created");
        self.configure_and_verify_new_toplevel(connection, window, surface);
        surface
    }

    #[track_caller]
    fn map_as_popup(
        &mut self,
        connection: &mut Connection,
        window: x::Window,
    ) -> testwl::SurfaceId {
        connection.map_window(window);
        self.wait_and_dispatch();
        let surface = self
            .testwl
            .last_created_surface_id()
            .expect("No surface created");
        let data = self.testwl.get_surface_data(surface).unwrap();
        assert!(
            matches!(data.role, Some(testwl::SurfaceRole::Popup(_))),
            "surface role was wrong: {:?}",
            data.role
        );
        self.testwl.configure_popup(surface);
        self.wait_and_dispatch();
        surface
    }

    /// Triggers a Wayland side toplevel Close event and processes the corresponding
    /// X11 side WM_DELETE_WINDOW client message
    fn wm_delete_window(
        &mut self,
        connection: &mut Connection,
        window: x::Window,
        surface: testwl::SurfaceId,
    ) {
        connection.set_property(
            window,
            x::ATOM_ATOM,
            connection.atoms.wm_protocols,
            &[connection.atoms.wm_delete_window],
        );
        self.testwl.close_toplevel(surface);

        let event = connection.await_event();
        let xcb::Event::X(x::Event::ClientMessage(event)) = event else {
            panic!("Expected ClientMessage event, got {event:?}");
        };

        assert_eq!(event.window(), window);
        assert_eq!(event.format(), 32);
        assert_eq!(event.r#type(), connection.atoms.wm_protocols);
        match event.data() {
            x::ClientMessageData::Data32(d) => {
                assert_eq!(d[0], connection.atoms.wm_delete_window.resource_id())
            }
            other => panic!("wrong data type: {other:?}"),
        }
    }

    fn create_output(&mut self, x: i32, y: i32) -> wayland_server::protocol::wl_output::WlOutput {
        self.testwl.new_output(x, y);
        self.wait_and_dispatch();
        self.testwl.finalize_output()
    }
}

xcb::atoms_struct! {
    struct Atoms {
        wm_protocols => b"WM_PROTOCOLS",
        net_active_window => b"_NET_ACTIVE_WINDOW",
        wm_delete_window => b"WM_DELETE_WINDOW",
        net_wm_state => b"_NET_WM_STATE",
        skip_taskbar => b"_NET_WM_STATE_SKIP_TASKBAR",
        transient_for => b"WM_TRANSIENT_FOR",
        clipboard => b"CLIPBOARD",
        primary => b"PRIMARY",
        targets => b"TARGETS",
        multiple => b"MULTIPLE",
        wm_state => b"WM_STATE",
        wm_check => b"_NET_SUPPORTING_WM_CHECK",
        win_type => b"_NET_WM_WINDOW_TYPE",
        win_type_normal => b"_NET_WM_WINDOW_TYPE_NORMAL",
        win_type_menu => b"_NET_WM_WINDOW_TYPE_MENU",
        win_type_popup_menu => b"_NET_WM_WINDOW_TYPE_POPUP_MENU",
        win_type_dropdown_menu => b"_NET_WM_WINDOW_TYPE_DROPDOWN_MENU",
        win_type_tooltip => b"_NET_WM_WINDOW_TYPE_TOOLTIP",
        win_type_utility => b"_NET_WM_WINDOW_TYPE_UTILITY",
        win_type_dnd => b"_NET_WM_WINDOW_TYPE_DND",
        win_type_combo => b"_NET_WM_WINDOW_TYPE_COMBO",
        motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false,
        wm_hints => b"WM_HINTS",
        mime1 => b"text/plain" only_if_exists = false,
        mime2 => b"blah/blah" only_if_exists = false,
        incr => b"INCR",
        xsettings => b"_XSETTINGS_S0",
        xsettings_setting => b"_XSETTINGS_SETTINGS",
        moveresize => b"_NET_WM_MOVERESIZE",
    }
}

struct Settings {
    serial: u32,
    data: HashMap<String, Setting>,
}
struct Setting {
    value: i32,
    last_change: u32,
}

struct Connection {
    inner: xcb::Connection,
    pollfd: PollFd<'static>,
    atoms: Atoms,
    root: x::Window,
    visual: u32,
    wm_window: x::Window,
}

impl std::ops::Deref for Connection {
    type Target = xcb::Connection;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl Connection {
    fn new(display: &str) -> Self {
        let (inner, _) =
            xcb::Connection::connect_with_extensions(Some(display), &[xcb::Extension::XFixes], &[])
                .unwrap();
        // xfixes init
        let reply = inner
            .wait_for_reply(inner.send_request(&xcb::xfixes::QueryVersion {
                client_major_version: 1,
                client_minor_version: 0,
            }))
            .unwrap();
        assert_eq!(reply.major_version(), 1);

        let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) };
        let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
        let atoms = Atoms::intern_all(&inner).unwrap();
        let screen = inner.get_setup().roots().next().unwrap();
        let root = screen.root();
        let visual = screen.root_visual();

        let wm_window: x::Window = inner
            .wait_for_reply(inner.send_request(&x::GetProperty {
                delete: false,
                window: root,
                property: atoms.wm_check,
                r#type: x::ATOM_WINDOW,
                long_offset: 0,
                long_length: 1,
            }))
            .expect("Couldn't get WM window")
            .value()[0];

        Self {
            inner,
            pollfd,
            atoms,
            root,
            visual,
            wm_window,
        }
    }

    fn new_window(
        &self,
        parent: x::Window,
        x: i16,
        y: i16,
        width: u16,
        height: u16,
        override_redirect: bool,
    ) -> x::Window {
        let wid = self.inner.generate_id();
        let req = x::CreateWindow {
            depth: 0,
            wid,
            parent,
            x,
            y,
            width,
            height,
            border_width: 0,
            class: x::WindowClass::InputOutput,
            visual: self.visual,
            value_list: &[x::Cw::OverrideRedirect(override_redirect)],
        };
        self.inner
            .send_and_check_request(&req)
            .expect("creating window failed");

        wid
    }

    #[track_caller]
    fn map_window(&self, window: x::Window) {
        self.send_and_check_request(&x::MapWindow { window })
            .unwrap();
    }

    #[track_caller]
    fn unmap_window(&self, window: x::Window) {
        self.send_and_check_request(&x::UnmapWindow { window })
            .unwrap();
    }

    #[track_caller]
    fn destroy_window(&self, window: x::Window) {
        self.send_and_check_request(&x::DestroyWindow { window })
            .unwrap();
    }

    #[track_caller]
    fn set_property<P: x::PropEl>(
        &self,
        window: x::Window,
        r#type: x::Atom,
        property: x::Atom,
        data: &[P],
    ) {
        self.send_and_check_request(&x::ChangeProperty {
            mode: x::PropMode::Replace,
            window,
            r#type,
            property,
            data,
        })
        .unwrap();
    }

    #[track_caller]
    #[must_use]
    fn await_event(&mut self) -> xcb::Event {
        if let Some(event) = self.poll_for_event().expect("Failed to poll for event") {
            return event;
        }
        let timeout = timespec_from_millis(100);
        assert!(
            poll(&mut [self.pollfd.clone()], Some(&timeout)).expect("poll failed") > 0,
            "Did not get any X11 events"
        );
        self.pollfd.clear_revents();
        self.poll_for_event()
            .expect("Failed to poll for event after pollfd")
            .unwrap()
    }

    #[track_caller]
    fn get_reply<R: xcb::Request>(
        &self,
        req: &R,
    ) -> <R::Cookie as xcb::CookieWithReplyChecked>::Reply
    where
        R::Cookie: xcb::CookieWithReplyChecked,
    {
        self.wait_for_reply(self.send_request(req)).unwrap()
    }

    #[track_caller]
    fn set_selection_owner(&self, window: x::Window, selection: x::Atom) {
        self.send_and_check_request(&x::SetSelectionOwner {
            owner: window,
            selection,
            time: x::CURRENT_TIME,
        })
        .unwrap();
        let owner = self.get_reply(&x::GetSelectionOwner { selection });

        assert_eq!(window, owner.owner(), "Unexpected selection owner");
    }

    #[track_caller]
    fn await_selection_request(&mut self) -> x::SelectionRequestEvent {
        match self.await_event() {
            xcb::Event::X(x::Event::SelectionRequest(r)) => r,
            other => panic!("Didn't get selection request event, instead got {other:?}"),
        }
    }

    #[track_caller]
    fn await_selection_notify(&mut self) -> x::SelectionNotifyEvent {
        match self.await_event() {
            xcb::Event::X(x::Event::SelectionNotify(r)) => r,
            other => panic!("Didn't get selection notify event, instead got {other:?}"),
        }
    }

    #[track_caller]
    fn send_selection_notify(&self, request: &x::SelectionRequestEvent) {
        self.send_and_check_request(&x::SendEvent {
            propagate: false,
            destination: x::SendEventDest::Window(request.requestor()),
            event_mask: x::EventMask::empty(),
            event: &x::SelectionNotifyEvent::new(
                request.time(),
                request.requestor(),
                request.selection(),
                request.target(),
                request.property(),
            ),
        })
        .unwrap();
    }

    #[track_caller]
    fn await_property_notify(&mut self) -> x::PropertyNotifyEvent {
        match self.await_event() {
            xcb::Event::X(x::Event::PropertyNotify(r)) => r,
            other => panic!("Didn't get property notify event, instead got {other:?}"),
        }
    }

    #[track_caller]
    fn get_property_change_events(&self, window: x::Window) {
        self.send_and_check_request(&x::ChangeWindowAttributes {
            window,
            value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)],
        })
        .unwrap();
    }

    #[track_caller]
    fn verify_clipboard_owner(&self, window: x::Window) {
        let owner = self.get_reply(&x::GetSelectionOwner {
            selection: self.atoms.clipboard,
        });
        assert_eq!(owner.owner(), window, "Clipboard owner does not match");
    }

    #[track_caller]
    fn await_selection_owner_change(&mut self) -> xcb::xfixes::SelectionNotifyEvent {
        match self.await_event() {
            xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => e,
            other => panic!("Expected XFixes SelectionNotify, got {other:?}"),
        }
    }

    #[track_caller]
    fn get_selection_owner_change_events(&self, enable: bool, window: x::Window) {
        let event_mask = if enable {
            xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER
        } else {
            xcb::xfixes::SelectionEventMask::empty()
        };
        self.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
            window,
            selection: self.atoms.clipboard,
            event_mask,
        })
        .unwrap();
    }

    #[track_caller]
    fn send_client_message(&self, request: &x::ClientMessageEvent) {
        self.send_and_check_request(&x::SendEvent {
            propagate: false,
            destination: x::SendEventDest::Window(self.root),
            event_mask: x::EventMask::SUBSTRUCTURE_NOTIFY | x::EventMask::SUBSTRUCTURE_REDIRECT,
            event: request,
        })
        .unwrap();
    }

    fn get_xsettings(&self) -> Settings {
        let owner = self
            .get_reply(&x::GetSelectionOwner {
                selection: self.atoms.xsettings,
            })
            .owner();

        let reply = self.get_reply(&x::GetProperty {
            delete: false,
            window: owner,
            property: self.atoms.xsettings_setting,
            r#type: self.atoms.xsettings_setting,
            long_offset: 0,
            long_length: 60,
        });
        assert_eq!(reply.r#type(), self.atoms.xsettings_setting);

        let data = reply.value::<u8>();

        let byte_order = data[0];
        assert_eq!(byte_order, 0);
        let serial = u32::from_le_bytes(data[4..8].try_into().unwrap());
        let num_settings = u32::from_le_bytes(data[8..12].try_into().unwrap());

        let mut current_idx = 12;
        let mut settings = HashMap::new();
        for _ in 0..num_settings {
            assert_eq!(&data[current_idx..current_idx + 2], &[0, 0]);
            let name_len =
                u16::from_le_bytes(data[current_idx + 2..current_idx + 4].try_into().unwrap());

            let padding_start = current_idx + 4 + name_len as usize;
            let name = String::from_utf8(data[current_idx + 4..padding_start].to_vec()).unwrap();
            let num_padding_bytes = (4 - (name_len as usize % 4)) % 4;
            let data_start = padding_start + num_padding_bytes;
            let last_change =
                u32::from_le_bytes(data[data_start..data_start + 4].try_into().unwrap());
            let value =
                i32::from_le_bytes(data[data_start + 4..data_start + 8].try_into().unwrap());

            settings.insert(name, Setting { value, last_change });
            current_idx = data_start + 8;
        }

        Settings {
            serial,
            data: settings,
        }
    }
}

#[test]
fn toplevel_flow() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);
    let window = connection.new_window(connection.root, 0, 0, 200, 200, false);

    // Pre-map properties
    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_NAME,
        c"window".to_bytes(),
    );
    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_CLASS,
        &[
            c"instance".to_bytes_with_nul(),
            c"class".to_bytes_with_nul(),
        ]
        .concat(),
    );

    let flags = (WmSizeHintsFlags::ProgramMaxSize | WmSizeHintsFlags::ProgramMinSize).bits();
    connection.set_property(
        window,
        x::ATOM_WM_SIZE_HINTS,
        x::ATOM_WM_NORMAL_HINTS,
        &[flags, 0, 0, 0, 0, 50, 100, 300, 400],
    );
    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_NAME,
        c"window".to_bytes(),
    );
    connection.map_window(window);
    f.wait_and_dispatch();

    let reply = connection.get_reply(&x::GetProperty {
        delete: false,
        window,
        property: connection.atoms.wm_state,
        r#type: connection.atoms.wm_state,
        long_offset: 0,
        long_length: 1,
    });
    assert_eq!(reply.value::<u32>(), &[WmState::Normal as u32]);

    let surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created!");
    f.configure_and_verify_new_toplevel(&mut connection, window, surface);

    let data = f.testwl.get_surface_data(surface).unwrap();
    assert_eq!(data.toplevel().title, Some("window".into()));
    assert_eq!(data.toplevel().app_id, Some("class".into()));
    assert_eq!(
        data.toplevel().min_size,
        Some(testwl::Vec2 { x: 50, y: 100 })
    );
    assert_eq!(
        data.toplevel().max_size,
        Some(testwl::Vec2 { x: 300, y: 400 })
    );

    // Post map properties
    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_NAME,
        c"bindow".to_bytes(),
    );
    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_CLASS,
        c"boink".to_bytes(),
    );
    connection.set_property(
        window,
        x::ATOM_WM_SIZE_HINTS,
        x::ATOM_WM_NORMAL_HINTS,
        &[flags, 1, 2, 3, 4, 25, 50, 150, 200],
    );
    f.wait_and_dispatch();
    let data = f.testwl.get_surface_data(surface).unwrap();
    let toplevel = data.toplevel().toplevel.clone();
    assert_eq!(data.toplevel().title, Some("bindow".into()));
    assert_eq!(data.toplevel().app_id, Some("boink".into()));
    assert_eq!(
        data.toplevel().min_size,
        Some(testwl::Vec2 { x: 25, y: 50 })
    );
    assert_eq!(
        data.toplevel().max_size,
        Some(testwl::Vec2 { x: 150, y: 200 })
    );

    connection.unmap_window(window);
    f.wait_and_dispatch();

    let reply = connection.get_reply(&x::GetProperty {
        delete: false,
        window,
        property: connection.atoms.wm_state,
        r#type: connection.atoms.wm_state,
        long_offset: 0,
        long_length: 1,
    });
    assert_eq!(reply.value::<u32>(), &[WmState::Withdrawn as u32]);

    connection.map_window(window);
    f.wait_and_dispatch();
    let surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created!");
    f.configure_and_verify_new_toplevel(&mut connection, window, surface);

    f.wm_delete_window(&mut connection, window, surface);

    // Simulate killing client
    drop(connection);
    f.wait_and_dispatch();

    assert!(!toplevel.is_alive());
}

#[test]
fn reparent() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let parent = connection.new_window(connection.root, 0, 0, 1, 1, false);
    let child = connection.new_window(parent, 0, 0, 20, 20, false);

    connection
        .send_and_check_request(&x::ReparentWindow {
            window: child,
            parent: connection.root,
            x: 0,
            y: 0,
        })
        .unwrap();

    connection.map_window(child);
    f.wait_and_dispatch();
    let surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created!");
    f.configure_and_verify_new_toplevel(&mut connection, child, surface);
}

#[test]
fn window_properties_after_reparent() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let child = connection.new_window(connection.root, 0, 0, 1, 1, true);
    connection.map_window(child);
    f.wait_and_dispatch();
    let child_surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created!");
    f.configure_and_verify_new_toplevel(&mut connection, child, child_surface);

    let other = connection.new_window(connection.root, 0, 0, 100, 100, false);
    connection.map_window(other);
    f.wait_and_dispatch();
    let other_surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created!");
    f.configure_and_verify_new_toplevel(&mut connection, other, other_surface);

    connection.send_request(&x::UnmapWindow { window: child });
    let parent = connection.new_window(connection.root, 0, 0, 20, 20, false);

    connection.send_request(&x::ReparentWindow {
        window: child,
        parent,
        x: 0,
        y: 0,
    });

    // The server should get the notifications for these properties and shouldn't crash
    connection.set_property(
        child,
        x::ATOM_WM_SIZE_HINTS,
        x::ATOM_WM_NORMAL_HINTS,
        &[16u32, 0, 0, 0, 0, 200, 400],
    );
    connection.set_property(child, x::ATOM_STRING, x::ATOM_WM_NAME, b"title\0");
    connection.set_property(child, x::ATOM_STRING, x::ATOM_WM_CLASS, c"class".to_bytes());

    f.wait_and_dispatch();
}

#[test]
fn input_focus() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let wm_state = connection.atoms.wm_state;

    let conn = std::cell::RefCell::new(&mut connection);
    let check_focus = |win: x::Window| {
        let connection = conn.borrow();
        let focus = connection.get_reply(&x::GetInputFocus {}).focus();
        assert_eq!(win, focus);

        let reply = connection.get_reply(&x::GetProperty {
            delete: false,
            window: connection.root,
            property: connection.atoms.net_active_window,
            r#type: x::ATOM_WINDOW,
            long_offset: 0,
            long_length: 1,
        });

        assert_eq!(&[win], reply.value::<x::Window>());
    };

    let mut create_win = || {
        let mut connection = conn.borrow_mut();
        let win = connection.new_window(connection.root, 0, 0, 20, 20, false);
        connection.map_window(win);
        f.wait_and_dispatch();
        let surface = f
            .testwl
            .last_created_surface_id()
            .expect("No surface created!");
        f.configure_and_verify_new_toplevel(&mut connection, win, surface);
        (win, surface)
    };

    let (win1, surface1) = create_win();
    check_focus(win1);
    let (win2, surface2) = create_win();
    check_focus(win2);

    // Simulate exclusive fullscreen clients that set window state to mimized on focus loss
    conn.borrow()
        .set_property(win1, wm_state, wm_state, &[WmState::Iconic as u32, 0]);

    f.testwl.focus_toplevel(surface1);
    // Seems the event doesn't get caught by wait_and_dispatch...
    std::thread::sleep(std::time::Duration::from_millis(10));
    check_focus(win1);
    assert_eq!(
        conn.borrow()
            .get_reply(&x::GetProperty {
                delete: false,
                window: win1,
                property: wm_state,
                r#type: wm_state,
                long_offset: 0,
                long_length: 1,
            })
            .value::<u32>()
            .first()
            .and_then(|state| WmState::try_from(*state).ok()),
        Some(WmState::Normal)
    );

    f.testwl.unfocus_toplevel();
    std::thread::sleep(std::time::Duration::from_millis(10));
    check_focus(x::WINDOW_NONE);

    f.testwl.focus_toplevel(surface2);
    std::thread::sleep(std::time::Duration::from_millis(10));
    check_focus(win2);

    conn.borrow().destroy_window(win2);
    f.wait_and_dispatch();
    check_focus(x::WINDOW_NONE);

    f.wm_delete_window(&mut connection, win1, surface1);
}

#[test]
fn activation_x11_to_x11() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let window1 = connection.new_window(connection.root, 0, 0, 20, 20, false);
    let surface1 = f.map_as_toplevel(&mut connection, window1);
    let window2 = connection.new_window(connection.root, 0, 0, 20, 20, false);
    let surface2 = f.map_as_toplevel(&mut connection, window2);

    f.testwl.focus_toplevel(surface2);
    std::thread::sleep(Duration::from_millis(10));
    connection.send_client_message(&x::ClientMessageEvent::new(
        window1,
        connection.atoms.net_active_window,
        x::ClientMessageData::Data32([2, x::CURRENT_TIME, 0, 0, 0]),
    ));
    f.wait_and_dispatch();

    assert_eq!(f.testwl.get_focused(), Some(surface1));
}

#[test]
fn quick_delete() {
    let mut f = Fixture::new();
    let connection = Connection::new(&f.display);

    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    connection.map_window(window);
    f.wait_and_dispatch();
    let surf = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created");
    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_NAME,
        c"bindow".to_bytes(),
    );
    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_CLASS,
        &[c"f".to_bytes_with_nul(), c"ssalc".to_bytes_with_nul()].concat(),
    );
    let flags = (WmSizeHintsFlags::ProgramMaxSize | WmSizeHintsFlags::ProgramMinSize).bits();
    connection.set_property(
        window,
        x::ATOM_WM_SIZE_HINTS,
        x::ATOM_WM_NORMAL_HINTS,
        &[flags, 1, 2, 3, 4, 25, 50, 150, 200],
    );
    connection
        .send_and_check_request(&x::ConfigureWindow {
            window,
            value_list: &[x::ConfigWindow::X(10), x::ConfigWindow::Y(40)],
        })
        .unwrap();
    f.testwl
        .configure_toplevel(surf, 100, 100, vec![xdg_toplevel::State::Activated]);
    connection.destroy_window(window);
    f.wait_and_dispatch();

    assert_eq!(f.testwl.get_surface_data(surf), None);
}

#[test]
fn copy_from_x11() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    f.map_as_toplevel(&mut connection, window);

    connection.set_selection_owner(window, connection.atoms.primary);
    connection.set_selection_owner(window, connection.atoms.clipboard);

    for _ in [connection.atoms.primary, connection.atoms.clipboard] {
        let request = connection.await_selection_request();
        assert_eq!(request.target(), connection.atoms.targets);
        connection.set_property(
            request.requestor(),
            x::ATOM_ATOM,
            request.property(),
            &[connection.atoms.mime1, connection.atoms.mime2],
        );
        connection.send_selection_notify(&request);
    }
    f.wait_and_dispatch();

    struct MimeData {
        mime: x::Atom,
        data: testwl::PasteData,
    }
    let mimes_truth = [
        MimeData {
            mime: connection.atoms.mime1,
            data: testwl::PasteData {
                mime_type: "text/plain".to_string(),
                data: b"hello world".to_vec(),
            },
        },
        MimeData {
            mime: connection.atoms.mime2,
            data: testwl::PasteData {
                mime_type: "blah/blah".to_string(),
                data: vec![1, 2, 3, 4],
            },
        },
    ];

    // When requesting both primary and clipboard simultaneously, the first to be requested erases
    // its TARGETS (which is the correct behavior of a GetProperty request), but the second still
    // tried to use this data and would come up empty.
    let primary_mimes = f.testwl.primary_source_mimes();
    let clipboard_mimes = f.testwl.data_source_mimes();
    assert_eq!(
        primary_mimes.len(),
        mimes_truth.len(),
        "Wrong number of advertised primary mimes: {primary_mimes:?}"
    );
    assert_eq!(
        clipboard_mimes.len(),
        mimes_truth.len(),
        "Wrong number of advertised clipboard mimes: {clipboard_mimes:?}"
    );
    for MimeData { data, .. } in &mimes_truth {
        assert!(
            primary_mimes.contains(&data.mime_type),
            "Missing mime type {}",
            data.mime_type
        );
        assert!(
            clipboard_mimes.contains(&data.mime_type),
            "Missing mime type {}",
            data.mime_type
        );
    }

    // Type annotations hint the compiler to use HRTBs (needed since this closure is reused).
    // See: https://users.rust-lang.org/t/implementation-of-fnonce-is-not-general-enough/68294/3
    let mut send_data_for_mime = |mime: &str, _: &mut testwl::Server| {
        let request = connection.await_selection_request();
        let data = mimes_truth
            .iter()
            .find(|data| data.data.mime_type == mime)
            .unwrap_or_else(|| panic!("Asked for unknown mime: {mime}"));
        connection.set_property(
            request.requestor(),
            data.mime,
            request.property(),
            &data.data.data,
        );
        connection.send_selection_notify(&request);
        true
    };

    let clipboard_data = f.testwl.clipboard_paste_data(&mut send_data_for_mime);
    let primary_data = f.testwl.primary_paste_data(&mut send_data_for_mime);

    for data in [primary_data, clipboard_data] {
        let mut found_mimes = Vec::new();
        for testwl::PasteData { mime_type, data } in data {
            match &mime_type {
                x if x == "text/plain" => {
                    assert_eq!(&data, b"hello world");
                }
                x if x == "blah/blah" => {
                    assert_eq!(&data, &[1, 2, 3, 4]);
                }
                other => panic!("unexpected mime type: {other} ({data:?})"),
            }
            found_mimes.push(mime_type);
        }

        assert!(
            found_mimes.contains(&"text/plain".to_string()),
            "Didn't get mime data for text/plain"
        );
        assert!(
            found_mimes.contains(&"blah/blah".to_string()),
            "Didn't get mime data for blah/blah"
        );
    }
}

#[test]
fn copy_from_wayland() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    connection.get_selection_owner_change_events(true, window);
    f.map_as_toplevel(&mut connection, window);
    let offer = vec![
        testwl::PasteData {
            mime_type: "text/plain".into(),
            data: b"boingloings".to_vec(),
        },
        testwl::PasteData {
            mime_type: "yah/hah".into(),
            data: vec![1, 2, 3, 2, 1],
        },
    ];

    f.testwl.create_data_offer(offer.clone());
    connection.await_selection_owner_change();
    connection.verify_clipboard_owner(connection.wm_window);
    connection.get_selection_owner_change_events(false, window);

    let dest1_atom = connection
        .get_reply(&x::InternAtom {
            name: b"dest1",
            only_if_exists: false,
        })
        .atom();

    connection
        .send_and_check_request(&x::ConvertSelection {
            requestor: window,
            selection: connection.atoms.clipboard,
            target: connection.atoms.targets,
            property: dest1_atom,
            time: x::CURRENT_TIME,
        })
        .unwrap();

    let request = connection.await_selection_notify();
    assert_eq!(request.requestor(), window);
    assert_eq!(request.selection(), connection.atoms.clipboard);
    assert_eq!(request.target(), connection.atoms.targets);
    assert_eq!(request.property(), dest1_atom);

    let reply = connection.get_reply(&x::GetProperty {
        delete: true,
        window,
        property: dest1_atom,
        r#type: x::ATOM_ATOM,
        long_offset: 0,
        long_length: 10,
    });
    let targets: &[x::Atom] = reply.value();
    assert_eq!(targets.len(), 2);

    for testwl::PasteData { mime_type, data } in offer {
        let atom = connection
            .get_reply(&x::InternAtom {
                only_if_exists: true,
                name: mime_type.as_bytes(),
            })
            .atom();
        assert_ne!(atom, x::ATOM_NONE);
        assert!(targets.contains(&atom));

        std::thread::sleep(std::time::Duration::from_millis(50));
        connection
            .send_and_check_request(&x::ConvertSelection {
                requestor: window,
                selection: connection.atoms.clipboard,
                target: atom,
                property: dest1_atom,
                time: x::CURRENT_TIME,
            })
            .unwrap();

        f.wait_and_dispatch();
        let request = connection.await_selection_notify();
        assert_eq!(request.requestor(), window);
        assert_eq!(request.selection(), connection.atoms.clipboard);
        assert_eq!(request.target(), atom);
        assert_eq!(request.property(), dest1_atom);

        let val: Vec<u8> = connection
            .get_reply(&x::GetProperty {
                delete: true,
                window,
                property: dest1_atom,
                r#type: x::ATOM_ANY,
                long_offset: 0,
                long_length: 10,
            })
            .value()
            .to_vec();

        assert_eq!(val, data);
    }
}

#[test]
fn incr_copy_from_wayland() {
    const BYTES: usize = 3000;
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    connection.get_selection_owner_change_events(true, window);
    f.map_as_toplevel(&mut connection, window);
    let mut offer = vec![testwl::PasteData {
        mime_type: "text/plain".into(),
        data: std::iter::successors(Some(0u8), |n| Some(n.wrapping_add(1)))
            .take(BYTES)
            .collect(),
    }];

    f.testwl.create_data_offer(offer.clone());
    connection.await_selection_owner_change();
    connection.verify_clipboard_owner(connection.wm_window);
    connection.get_selection_owner_change_events(false, window);

    let dest1_atom = connection
        .get_reply(&x::InternAtom {
            name: b"dest1",
            only_if_exists: false,
        })
        .atom();

    connection
        .send_and_check_request(&x::ConvertSelection {
            requestor: window,
            selection: connection.atoms.clipboard,
            target: connection.atoms.targets,
            property: dest1_atom,
            time: x::CURRENT_TIME,
        })
        .unwrap();

    let request = connection.await_selection_notify();
    assert_eq!(request.requestor(), window);
    assert_eq!(request.selection(), connection.atoms.clipboard);
    assert_eq!(request.target(), connection.atoms.targets);
    assert_eq!(request.property(), dest1_atom);

    let reply = connection.get_reply(&x::GetProperty {
        delete: true,
        window,
        property: dest1_atom,
        r#type: x::ATOM_ATOM,
        long_offset: 0,
        long_length: 10,
    });
    let targets: &[x::Atom] = reply.value();
    assert_eq!(targets.len(), 1);

    let offer_data = offer.pop().unwrap();
    let (mime_type, data) = (offer_data.mime_type, offer_data.data);
    let atom = connection
        .get_reply(&x::InternAtom {
            only_if_exists: true,
            name: mime_type.as_bytes(),
        })
        .atom();
    assert_ne!(atom, x::ATOM_NONE);
    assert!(targets.contains(&atom));

    connection
        .send_and_check_request(&x::ConvertSelection {
            requestor: window,
            selection: connection.atoms.clipboard,
            target: atom,
            property: dest1_atom,
            time: x::CURRENT_TIME,
        })
        .unwrap();

    f.wait_and_dispatch();
    let request = connection.await_selection_notify();
    assert_eq!(request.requestor(), window);
    assert_eq!(request.selection(), connection.atoms.clipboard);
    assert_eq!(request.target(), atom);
    assert_eq!(request.property(), dest1_atom);

    connection.get_property_change_events(window);
    let reply = connection.get_reply(&x::GetProperty {
        delete: true,
        window,
        property: dest1_atom,
        r#type: x::ATOM_ANY,
        long_offset: 0,
        long_length: (BYTES / 4 + BYTES % 4) as u32,
    });
    assert_eq!(reply.r#type(), connection.atoms.incr);
    assert_eq!(reply.value::<u32>().len(), 1);
    assert!(reply.value::<u32>()[0] <= data.len() as u32);

    let delete_property = connection.await_property_notify();
    assert_eq!(delete_property.state(), x::Property::Delete);
    assert_eq!(delete_property.atom(), dest1_atom);

    for (idx, chunk) in data.chunks(500).chain([]).enumerate() {
        let new_property = connection.await_property_notify();
        assert_eq!(new_property.state(), x::Property::NewValue, "chunk {idx}");
        assert_eq!(new_property.atom(), dest1_atom, "chunk {idx}");

        let incr_reply = connection.get_reply(&x::GetProperty {
            delete: true,
            window,
            property: dest1_atom,
            r#type: x::ATOM_ANY,
            long_offset: 0,
            long_length: (BYTES / 4 + BYTES % 4) as u32,
        });
        assert_eq!(incr_reply.r#type(), atom, "chunk {idx}");
        assert_eq!(incr_reply.value::<u8>(), chunk, "chunk {idx}");

        let delete_property = connection.await_property_notify();
        assert_eq!(delete_property.state(), x::Property::Delete, "chunk {idx}");
        assert_eq!(delete_property.atom(), dest1_atom, "chunk {idx}");
    }
}

// TODO: this test doesn't actually match real behavior for some reason...
#[test]
fn different_output_position() {
    let mut f = Fixture::new();
    //f.testwl.enable_xdg_output_manager();
    let mut connection = Connection::new(&f.display);

    let window = connection.new_window(connection.root, 0, 0, 200, 200, false);
    connection.map_window(window);
    f.wait_and_dispatch();
    let surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created!");
    f.configure_and_verify_new_toplevel(&mut connection, window, surface);

    let output = f.create_output(0, 0);
    f.testwl.move_surface_to_output(surface, &output);
    f.testwl.move_pointer_to(surface, 10.0, 10.0);
    f.wait_and_dispatch();
    let reply = connection.get_reply(&x::QueryPointer { window });
    assert!(reply.same_screen());
    assert_eq!(reply.win_x(), 10);
    assert_eq!(reply.win_y(), 10);

    let output = f.create_output(100, 0);
    //f.testwl.move_xdg_output(&output, 100, 0);
    f.testwl.move_surface_to_output(surface, &output);
    f.testwl.move_pointer_to(surface, 150.0, 12.0);
    f.wait_and_dispatch();
    let reply = connection.get_reply(&x::QueryPointer { window });
    assert!(reply.same_screen());
    assert_eq!(reply.win_x(), 150);
    assert_eq!(reply.win_y(), 12);
}

#[test]
fn bad_clipboard_data() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);
    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    f.map_as_toplevel(&mut connection, window);
    connection.set_selection_owner(window, connection.atoms.clipboard);

    let request = connection.await_selection_request();
    assert_eq!(request.target(), connection.atoms.targets);
    connection.set_property(
        request.requestor(),
        x::ATOM_ATOM,
        request.property(),
        &[connection.atoms.mime2],
    );
    connection.send_selection_notify(&request);

    f.wait_and_dispatch();
    let mut data = f.testwl.clipboard_paste_data(|_, _| {
        let request = connection.await_selection_request();
        assert_eq!(request.target(), connection.atoms.mime2);
        // Don't actually set any data as requested - just report success
        connection.send_selection_notify(&request);
        true
    });

    connection.verify_clipboard_owner(window);
    assert_eq!(data.len(), 1, "Unexpected data: {data:?}");
    let data = data.pop().unwrap();
    assert_eq!(data.mime_type, "blah/blah");
    assert!(data.data.is_empty(), "Unexpected data: {:?}", data.data);
}

// issue #42
#[test]
fn funny_window_title() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);
    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    connection.set_property(window, x::ATOM_STRING, x::ATOM_WM_NAME, b"title\0\0\0\0");
    connection.map_window(window);
    f.wait_and_dispatch();

    let surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created!");
    f.configure_and_verify_new_toplevel(&mut connection, window, surface);
    let data = f.testwl.get_surface_data(surface).unwrap();
    assert_eq!(data.toplevel().title, Some("title".into()));

    connection.set_property(
        window,
        x::ATOM_STRING,
        x::ATOM_WM_NAME,
        b"title\0irrelevantdata\0",
    );
    f.wait_and_dispatch();

    let data = f.testwl.get_surface_data(surface).unwrap();
    assert_eq!(data.toplevel().title, Some("title".into()));
}

#[test]
fn close_window() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);
    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    connection.map_window(window);
    f.wait_and_dispatch();
    let surface = f
        .testwl
        .last_created_surface_id()
        .expect("No surface created");
    f.configure_and_verify_new_toplevel(&mut connection, window, surface);
    f.wm_delete_window(&mut connection, window, surface);

    connection
        .send_and_check_request(&x::DeleteProperty {
            window,
            property: connection.atoms.wm_protocols,
        })
        .unwrap();
    f.testwl.close_toplevel(surface);
    f.wait_and_dispatch();

    // Connection should no longer work (KillClient)
    assert!(connection.poll_for_event().is_err());
}

// TODO: figure out if the sleeps in this test can be dealt with...
#[test]
fn primary_output() {
    let mut f = Fixture::new_preset(|testwl| {
        testwl.new_output(0, 0); // WL-1
        testwl.new_output(500, 500); // WL-2
    });
    let mut conn = Connection::new(&f.display);

    let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root });
    let config_timestamp = reply.config_timestamp();
    let mut it = reply.outputs().iter().copied().map(|output| {
        let reply = conn.get_reply(&xcb::randr::GetOutputInfo {
            output,
            config_timestamp,
        });
        let name = std::str::from_utf8(reply.name()).unwrap();
        (
            f.testwl
                .get_output(name)
                .unwrap_or_else(|| panic!("Couldn't find output {name}")),
            output,
        )
    });
    let (wl_output1, output1) = it.next().expect("Couldn't find first output");
    let (wl_output2, output2) = it.next().expect("Couldn't find second output");
    assert_eq!(it.collect::<Vec<_>>(), vec![]);

    let window1 = conn.new_window(conn.root, 0, 0, 20, 20, false);
    let surface1 = f.map_as_toplevel(&mut conn, window1);
    f.testwl.move_surface_to_output(surface1, &wl_output1);
    let window2 = conn.new_window(conn.root, 0, 0, 20, 20, false);
    std::thread::sleep(std::time::Duration::from_millis(10));
    let surface2 = f.map_as_toplevel(&mut conn, window2);
    f.testwl.move_surface_to_output(surface2, &wl_output2);
    assert_ne!(surface1, surface2);
    f.wait_and_dispatch();

    f.testwl.focus_toplevel(surface1);
    std::thread::sleep(std::time::Duration::from_millis(10));
    let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root });
    assert_eq!(reply.output(), output1);

    f.testwl.focus_toplevel(surface2);
    std::thread::sleep(std::time::Duration::from_millis(10));
    let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root });
    assert_eq!(reply.output(), output2);

    let wl_output3 = f.create_output(24, 46);
    f.testwl.move_surface_to_output(surface2, &wl_output3);
    std::thread::sleep(std::time::Duration::from_millis(10));

    let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root });
    assert_eq!(reply.outputs().len(), 3);
    let output3 = reply
        .outputs()
        .iter()
        .copied()
        .find(|o| ![output1, output2].contains(o))
        .unwrap();
    let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root });
    assert_eq!(reply.output(), output3);
}

#[test]
fn incr_copy_from_x11() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);
    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    f.map_as_toplevel(&mut connection, window);

    connection.set_selection_owner(window, connection.atoms.clipboard);
    let request = connection.await_selection_request();
    assert_eq!(request.target(), connection.atoms.targets);
    assert_eq!(request.selection(), connection.atoms.clipboard);
    connection.set_property(
        request.requestor(),
        x::ATOM_ATOM,
        request.property(),
        &[connection.atoms.targets, connection.atoms.mime1],
    );
    connection.send_selection_notify(&request);
    f.wait_and_dispatch();

    // Also give the window the primary selection.
    // Due to a bug introduced in primary selection support, `XState::selection_state` having both
    // a primary and clipboard X selection prevented clipboard INCR checks from occuring.
    connection.set_selection_owner(window, connection.atoms.primary);
    let request = connection.await_selection_request();
    assert_eq!(request.target(), connection.atoms.targets);
    assert_eq!(request.selection(), connection.atoms.primary);
    connection.set_property(
        request.requestor(),
        x::ATOM_ATOM,
        request.property(),
        &[connection.atoms.targets, connection.atoms.mime2],
    );
    connection.send_selection_notify(&request);
    f.wait_and_dispatch();

    let mut destination_property = x::Atom::none();
    let mut begin_incr = Some(|connection: &mut Connection| {
        let request = connection.await_selection_request();
        assert_eq!(request.target(), connection.atoms.mime1);

        connection.get_property_change_events(request.requestor());
        connection.set_property(
            request.requestor(),
            connection.atoms.incr,
            request.property(),
            &[3000u32],
        );
        connection.send_selection_notify(&request);
        // skip NewValue
        let notify = connection.await_property_notify();
        assert_eq!(notify.atom(), request.property());
        assert_eq!(notify.state(), x::Property::NewValue);
        request.property()
    });

    let data: Vec<u8> = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1)))
        .take(3000)
        .collect();
    let mut it = data.chunks(500).enumerate();
    let mut paste_data = f.testwl.clipboard_paste_data(|_, testwl| {
        if let Some(begin) = begin_incr.take() {
            destination_property = begin(&mut connection);
            testwl.dispatch();
            return false;
        }
        assert_ne!(destination_property, x::Atom::none());

        let notify = connection.await_property_notify();
        match it.next() {
            Some((idx, chunk)) => {
                assert_eq!(notify.atom(), destination_property, "chunk {idx}");
                assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}");
                connection.set_property(
                    notify.window(),
                    connection.atoms.mime1,
                    destination_property,
                    chunk,
                );
                testwl.dispatch();
                // skip NewValue
                let notify = connection.await_property_notify();
                assert_eq!(notify.atom(), destination_property, "chunk {idx}");
                assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}");
                false
            }
            None => {
                // INCR completed!
                assert_eq!(notify.atom(), destination_property);
                assert_eq!(notify.state(), x::Property::Delete);
                connection.set_property::<u8>(
                    notify.window(),
                    connection.atoms.mime1,
                    destination_property,
                    &[],
                );
                true
            }
        }
    });

    assert_eq!(f.testwl.data_source_mimes(), vec!["text/plain"]);
    assert_eq!(paste_data.len(), 1);
    let paste_data = paste_data.swap_remove(0);
    assert_eq!(paste_data.mime_type, "text/plain");
    assert_eq!(&paste_data.data, &data);
}

#[test]
fn wayland_then_x11_clipboard_owner() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);
    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    connection.get_selection_owner_change_events(true, window);

    f.map_as_toplevel(&mut connection, window);
    let offer = vec![
        testwl::PasteData {
            mime_type: "text/plain".into(),
            data: b"boingloings".to_vec(),
        },
        testwl::PasteData {
            mime_type: "yah/hah".into(),
            data: vec![1, 2, 3, 2, 1],
        },
    ];
    f.testwl.create_data_offer(offer.clone());

    connection.await_selection_owner_change();
    connection.verify_clipboard_owner(connection.wm_window);
    connection.get_selection_owner_change_events(false, window);

    connection.set_selection_owner(window, connection.atoms.clipboard);
    f.testwl.dispatch();
    connection.verify_clipboard_owner(window);

    let request = connection.await_selection_request();
    assert_eq!(request.selection(), connection.atoms.clipboard);
    assert_eq!(request.target(), connection.atoms.targets);
}

#[test]
fn fake_selection_targets() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);
    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    connection.get_selection_owner_change_events(true, window);

    let data = b"boingloings";
    f.map_as_toplevel(&mut connection, window);
    let offer = vec![testwl::PasteData {
        mime_type: "text/plain;charset=utf-8".into(),
        data: data.to_vec(),
    }];
    f.testwl.create_data_offer(offer.clone());

    connection.await_selection_owner_change();
    connection.verify_clipboard_owner(connection.wm_window);
    connection.get_selection_owner_change_events(false, window);

    let utf8_string = connection
        .get_reply(&x::InternAtom {
            only_if_exists: false,
            name: b"UTF8_STRING",
        })
        .atom();

    connection
        .send_and_check_request(&x::ConvertSelection {
            requestor: window,
            selection: connection.atoms.clipboard,
            target: utf8_string,
            property: utf8_string,
            time: x::CURRENT_TIME,
        })
        .unwrap();

    f.wait_and_dispatch();
    let notify = connection.await_selection_notify();
    assert_eq!(notify.property(), utf8_string, "ConvertSelection failed");

    let reply = connection.get_reply(&x::GetProperty {
        delete: false,
        window,
        property: utf8_string,
        r#type: utf8_string,
        long_offset: 0,
        long_length: data.len() as u32,
    });
    let paste_data: &[u8] = reply.value();

    assert_eq!(
        std::str::from_utf8(paste_data).unwrap(),
        std::str::from_utf8(data).unwrap()
    );
}

#[test]
fn popup_done() {
    let mut f = Fixture::new();
    let mut conn = Connection::new(&f.display);
    let toplevel = conn.new_window(conn.root, 0, 0, 20, 20, false);
    f.map_as_toplevel(&mut conn, toplevel);

    let popup = conn.new_window(conn.root, 0, 0, 20, 20, true);
    let surface = f.map_as_popup(&mut conn, popup);
    let geometry = conn.get_reply(&x::GetGeometry {
        drawable: x::Drawable::Window(popup),
    });

    assert_eq!(geometry.x(), 0);
    assert_eq!(geometry.y(), 0);
    assert_eq!(geometry.width(), 20);
    assert_eq!(geometry.height(), 20);

    f.testwl.popup_done(surface);
    f.wait_and_dispatch();

    let reply = conn
        .wait_for_reply(conn.send_request(&x::GetWindowAttributes { window: popup }))
        .expect("Couldn't get window attributes");

    assert_eq!(reply.map_state(), x::MapState::Unmapped);
}

#[test]
fn negative_output_coordinates() {
    let mut f = Fixture::new();
    let output = f.create_output(-500, -500);
    let mut connection = Connection::new(&f.display);

    let window = connection.new_window(connection.root, 0, 0, 200, 200, false);
    let surface = f.map_as_toplevel(&mut connection, window);
    f.testwl.move_surface_to_output(surface, &output);
    f.testwl.move_pointer_to(surface, 30.0, 40.0);
    f.wait_and_dispatch();

    let tree = connection.get_reply(&x::QueryTree { window });
    let geo = connection.get_reply(&x::GetGeometry {
        drawable: x::Drawable::Window(window),
    });
    let reply = connection.get_reply(&x::TranslateCoordinates {
        src_window: tree.parent(),
        dst_window: connection.root,
        src_x: geo.x(),
        src_y: geo.y(),
    });

    assert!(reply.same_screen());
    assert_eq!(reply.dst_x(), 0);
    assert_eq!(reply.dst_y(), 0);

    let ptr_reply = connection.get_reply(&x::QueryPointer {
        window: connection.root,
    });
    assert!(ptr_reply.same_screen());
    assert_eq!(ptr_reply.child(), window);
    assert_eq!(ptr_reply.win_x(), 30);
    assert_eq!(ptr_reply.win_y(), 40);
}

#[test]
fn xdg_decorations() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    let surface = f.map_as_toplevel(&mut connection, window);
    let data = f.testwl.get_surface_data(surface).unwrap();
    // The default decoration mode in x11 is SSD
    assert_eq!(
        data.toplevel()
            .decoration
            .as_ref()
            .and_then(|(_, decoration)| *decoration),
        Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
    );

    // CSD
    connection.set_property(
        window,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[2u32, 0, 0, 0, 0],
    );
    f.wait_and_dispatch();
    let data = f.testwl.get_surface_data(surface).unwrap();
    assert_eq!(
        data.toplevel()
            .decoration
            .as_ref()
            .and_then(|(_, decoration)| *decoration),
        Some(zxdg_toplevel_decoration_v1::Mode::ClientSide)
    );

    // SSD
    connection.set_property(
        window,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[2u32, 0, 1, 0, 0],
    );
    f.wait_and_dispatch();
    let data = f.testwl.get_surface_data(surface).unwrap();
    assert_eq!(
        data.toplevel()
            .decoration
            .as_ref()
            .and_then(|(_, decoration)| *decoration),
        Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
    );
}

#[test]
fn forced_1x_scale_consistent_x11_size() {
    let mut f = Fixture::new();
    f.testwl.enable_xdg_output_manager();
    let output = f.create_output(0, 0);
    output.scale(2);
    output.done();

    let mut conn = Connection::new(&f.display);
    let window = conn.new_window(conn.root, 0, 0, 200, 200, false);
    let surface = f.map_as_toplevel(&mut conn, window);
    f.testwl.move_surface_to_output(surface, &output);
    f.testwl.move_pointer_to(surface, 30.0, 40.0);
    f.wait_and_dispatch();

    let tree = conn.get_reply(&x::QueryTree { window });
    let geo = conn.get_reply(&x::GetGeometry {
        drawable: x::Drawable::Window(window),
    });
    let reply = conn.get_reply(&x::TranslateCoordinates {
        src_window: tree.parent(),
        dst_window: conn.root,
        src_x: geo.x(),
        src_y: geo.y(),
    });

    assert!(reply.same_screen());
    assert_eq!(reply.dst_x(), 0);
    assert_eq!(reply.dst_y(), 0);

    let ptr_reply = conn.get_reply(&x::QueryPointer { window: conn.root });
    assert!(ptr_reply.same_screen());
    assert_eq!(ptr_reply.child(), window);
    assert_eq!(ptr_reply.win_x(), 60);
    assert_eq!(ptr_reply.win_y(), 80);

    // Update scale
    output.scale(3);
    output.done();
    f.wait_and_dispatch();

    f.testwl
        .configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]);
    f.testwl.focus_toplevel(surface);
    f.testwl.move_pointer_to(surface, 30.0, 40.0);
    f.wait_and_dispatch();

    let ptr_reply = conn.get_reply(&x::QueryPointer { window: conn.root });
    assert!(ptr_reply.same_screen());
    assert_eq!(ptr_reply.child(), window);
    assert_eq!(ptr_reply.win_x(), 90);
    assert_eq!(ptr_reply.win_y(), 120);

    // Popup
    let popup = conn.new_window(conn.root, 60, 60, 30, 30, true);
    f.map_as_popup(&mut conn, popup);
    let tree = conn.get_reply(&x::QueryTree { window: popup });
    let geo = conn.get_reply(&x::GetGeometry {
        drawable: x::Drawable::Window(popup),
    });
    let reply = conn.get_reply(&x::TranslateCoordinates {
        src_window: tree.parent(),
        dst_window: conn.root,
        src_x: geo.x(),
        src_y: geo.y(),
    });

    assert_eq!(reply.dst_x(), 60);
    assert_eq!(reply.dst_y(), 60);
    assert_eq!(geo.width(), 30);
    assert_eq!(geo.height(), 30);
}

#[test]
fn popup_heuristics() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let win_toplevel = connection.new_window(connection.root, 0, 0, 20, 20, false);
    f.map_as_toplevel(&mut connection, win_toplevel);

    let ghidra_popup = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        ghidra_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_normal],
    );
    connection.set_property(
        ghidra_popup,
        x::ATOM_ATOM,
        connection.atoms.net_wm_state,
        &[connection.atoms.skip_taskbar],
    );
    connection.set_property(
        ghidra_popup,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0b11_u32, 0, 0, 0, 0],
    );
    f.map_as_popup(&mut connection, ghidra_popup);

    let reaper_dialog = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        ghidra_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_normal],
    );
    connection.set_property(
        ghidra_popup,
        x::ATOM_ATOM,
        connection.atoms.net_wm_state,
        &[connection.atoms.skip_taskbar],
    );
    connection.set_property(
        ghidra_popup,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x2_u32, 0, 0x2a, 0, 0],
    );
    f.map_as_toplevel(&mut connection, reaper_dialog);

    let chromium_menu = connection.new_window(connection.root, 10, 10, 50, 50, true);
    connection.set_property(
        chromium_menu,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_menu],
    );
    f.map_as_popup(&mut connection, chromium_menu);

    let chromium_tooltip = connection.new_window(connection.root, 10, 10, 50, 50, true);
    connection.set_property(
        chromium_tooltip,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_tooltip],
    );
    f.map_as_popup(&mut connection, chromium_tooltip);

    let discord_dnd = connection.new_window(connection.root, 20, 138, 48, 48, true);
    connection.set_property(
        discord_dnd,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_dnd],
    );
    f.map_as_popup(&mut connection, discord_dnd);

    let git_gui_popup = connection.new_window(connection.root, 10, 10, 50, 50, true);
    connection.set_property(
        git_gui_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_popup_menu],
    );
    f.map_as_popup(&mut connection, git_gui_popup);

    let git_gui_dropdown = connection.new_window(connection.root, 10, 10, 50, 50, true);
    connection.set_property(
        git_gui_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_dropdown_menu],
    );
    f.map_as_popup(&mut connection, git_gui_dropdown);

    let wechat_popup = connection.new_window(connection.root, 10, 10, 50, 50, true);
    connection.set_property(
        wechat_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_utility],
    );
    connection.set_property(
        wechat_popup,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x2_u32, 0, 0, 0, 0],
    );
    f.map_as_popup(&mut connection, wechat_popup);

    let fcitx5_popup = connection.new_window(connection.root, 10, 10, 50, 50, true);
    connection.set_property(
        fcitx5_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_combo],
    );
    f.map_as_popup(&mut connection, fcitx5_popup);

    let godot_popup = connection.new_window(connection.root, 10, 10, 50, 50, true);
    connection.set_property(
        godot_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_utility],
    );
    connection.set_property(
        godot_popup,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x2_u32, 0, 0, 0, 0],
    );
    f.map_as_popup(&mut connection, godot_popup);

    let material_maker_popup = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        material_maker_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_utility],
    );
    connection.set_property(
        material_maker_popup,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x2_u32, 0, 0, 0, 0],
    );
    f.map_as_popup(&mut connection, material_maker_popup);

    let ardour_toplevel = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        ardour_toplevel,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_utility],
    );
    f.map_as_toplevel(&mut connection, ardour_toplevel);

    let yabridge_popup = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        yabridge_popup,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_normal],
    );
    connection.set_property(
        yabridge_popup,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x2_u32, 0, 0, 0, 0],
    );
    connection.set_property(
        yabridge_popup,
        connection.atoms.wm_hints,
        connection.atoms.wm_hints,
        &[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0],
    );
    connection.set_property(
        yabridge_popup,
        x::ATOM_ATOM,
        connection.atoms.net_wm_state,
        &[connection.atoms.skip_taskbar],
    );
    f.map_as_popup(&mut connection, yabridge_popup);

    let steam = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        steam,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_normal],
    );
    connection.set_property(
        steam,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x2_u32, 0, 0, 0, 0],
    );
    connection.set_property(
        steam,
        connection.atoms.wm_hints,
        connection.atoms.wm_hints,
        &[0x1_u32, 1, 0, 0, 0, 0, 0, 0, 0],
    );
    f.map_as_toplevel(&mut connection, steam);

    let battle_net = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        battle_net,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_normal],
    );
    connection.set_property(
        battle_net,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x3_u32, 0x2c, 0x0, 0x0, 0x0],
    );
    connection.set_property(
        battle_net,
        connection.atoms.wm_hints,
        connection.atoms.wm_hints,
        &[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0],
    );
    f.map_as_toplevel(&mut connection, battle_net);

    let wallpaper_engine = connection.new_window(connection.root, 10, 10, 50, 50, false);
    connection.set_property(
        wallpaper_engine,
        x::ATOM_ATOM,
        connection.atoms.win_type,
        &[connection.atoms.win_type_normal],
    );
    connection.set_property(
        wallpaper_engine,
        connection.atoms.motif_wm_hints,
        connection.atoms.motif_wm_hints,
        &[0x3_u32, 0x6, 0x0, 0x0, 0x0],
    );
    connection.set_property(
        wallpaper_engine,
        connection.atoms.wm_hints,
        connection.atoms.wm_hints,
        &[0x1_u32, 0, 0, 0, 0, 0, 0, 0, 0],
    );
    f.map_as_toplevel(&mut connection, wallpaper_engine);
}

#[test]
fn xsettings_scale() {
    let mut f = Fixture::new_preset(|testwl| {
        testwl.new_output(0, 0); // WL-1
    });
    let connection = Connection::new(&f.display);
    f.testwl.enable_xdg_output_manager();

    let settings = connection.get_xsettings();
    let settings_serial = settings.serial;
    assert_eq!(settings.data["Xft/DPI"].value, 96 * 1024);
    let dpi_serial = settings.data["Xft/DPI"].last_change;
    assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1);
    let window_serial = settings.data["Gdk/WindowScalingFactor"].last_change;
    assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024);
    let unscaled_serial = settings.data["Gdk/UnscaledDPI"].last_change;

    let output = f.testwl.get_output("WL-1").unwrap();
    output.scale(2);
    output.done();
    f.wait_and_dispatch();

    let settings = connection.get_xsettings();
    assert!(settings.serial > settings_serial);
    assert_eq!(settings.data["Xft/DPI"].value, 2 * 96 * 1024);
    assert!(settings.data["Xft/DPI"].last_change > dpi_serial);
    assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2);
    assert!(settings.data["Gdk/WindowScalingFactor"].last_change > window_serial);
    assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024);
    assert!(settings.data["Gdk/UnscaledDPI"].last_change > unscaled_serial);

    let output2 = f.create_output(0, 0);
    let settings = connection.get_xsettings();
    assert_eq!(settings.data["Xft/DPI"].value, 96 * 1024);
    assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1);
    assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024);

    output2.scale(2);
    output2.done();
    f.testwl.dispatch();
    std::thread::sleep(Duration::from_millis(1));

    let settings = connection.get_xsettings();
    assert_eq!(settings.data["Xft/DPI"].value, 2 * 96 * 1024);
    assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2);
    assert_eq!(settings.data["Gdk/UnscaledDPI"].value, 96 * 1024);
}

#[test]
fn xsettings_fractional_scale() {
    let mut f = Fixture::new_preset(|testwl| {
        testwl.new_output(0, 0); // WL-1
        testwl.enable_fractional_scale();
    });
    let mut connection = Connection::new(&f.display);
    f.testwl.enable_xdg_output_manager();

    let output = f.testwl.finalize_output();

    let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
    let surface = f.map_as_toplevel(&mut connection, window);

    let data = f
        .testwl
        .get_surface_data(surface)
        .expect("Missing surface data");
    let fractional = data
        .fractional
        .as_ref()
        .expect("No fractional scale for surface");

    fractional.preferred_scale(180); // 1.5 scale
    f.testwl.move_surface_to_output(surface, &output);

    f.wait_and_dispatch();
    let settings = connection.get_xsettings();

    assert_eq!(
        settings.data["Xft/DPI"].value,
        (1.5 * 96_f64 * 1024_f64).round() as i32
    );
    assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 1);
    assert_eq!(
        settings.data["Gdk/UnscaledDPI"].value,
        (1.5 * 96_f64 * 1024_f64).round() as i32
    );

    let data = f.testwl.get_surface_data(surface).unwrap();
    let fractional = data.fractional.as_ref().unwrap();
    fractional.preferred_scale(300); // 2.5 scale
    f.wait_and_dispatch();

    let settings = connection.get_xsettings();
    assert_eq!(
        settings.data["Xft/DPI"].value,
        (2.5 * 96_f64 * 1024_f64).round() as i32
    );
    assert_eq!(settings.data["Gdk/WindowScalingFactor"].value, 2);
    assert_eq!(
        settings.data["Gdk/UnscaledDPI"].value,
        (2.5 / 2.0 * 96_f64 * 1024_f64).round() as i32
    );
}

#[test]
fn xsettings_switch_owner() {
    let f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let owner = connection
        .get_reply(&x::GetSelectionOwner {
            selection: connection.atoms.xsettings,
        })
        .owner();

    let win = connection.generate_id();
    connection
        .send_and_check_request(&x::CreateWindow {
            wid: win,
            x: 0,
            y: 0,
            parent: connection.root,
            depth: 0,
            width: 1,
            height: 1,
            border_width: 0,
            class: x::WindowClass::InputOnly,
            visual: x::COPY_FROM_PARENT,
            value_list: &[],
        })
        .unwrap();

    connection
        .send_and_check_request(&x::SetSelectionOwner {
            owner: win,
            selection: connection.atoms.xsettings,
            time: x::CURRENT_TIME,
        })
        .unwrap();

    assert_eq!(
        connection
            .get_reply(&x::GetSelectionOwner {
                selection: connection.atoms.xsettings,
            })
            .owner(),
        win
    );

    connection
        .send_and_check_request(&xcb::xfixes::SelectSelectionInput {
            window: connection.root,
            selection: connection.atoms.xsettings,
            event_mask: xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER
                | xcb::xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY
                | xcb::xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE,
        })
        .unwrap();

    connection
        .send_and_check_request(&x::DestroyWindow { window: win })
        .unwrap();

    match connection.await_event() {
        xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(x))
            if x.subtype() == xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {}
        other => panic!("unexpected event {other:?}"),
    }
    match connection.await_event() {
        xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(x))
            if x.subtype() == xcb::xfixes::SelectionEvent::SetSelectionOwner => {}
        other => panic!("unexpected event {other:?}"),
    }

    assert_eq!(
        connection
            .get_reply(&x::GetSelectionOwner {
                selection: connection.atoms.xsettings,
            })
            .owner(),
        owner
    );
}

#[test]
fn rotated_output() {
    let mut f = Fixture::new_preset(|testwl| {
        testwl.enable_xdg_output_manager();
        testwl.new_output(0, 0);
    });
    let mut connection = Connection::new(&f.display);

    connection
        .send_and_check_request(&x::ChangeWindowAttributes {
            window: connection.root,
            value_list: &[x::Cw::EventMask(x::EventMask::STRUCTURE_NOTIFY)],
        })
        .unwrap();

    let output = f.testwl.get_output("WL-1").unwrap();
    output.mode(wl_output::Mode::Current, 100, 1000, 60);
    output.geometry(
        0,
        0,
        50,
        50,
        wl_output::Subpixel::Unknown,
        "satellite".to_string(),
        "WL-1".to_string(),
        wl_output::Transform::_90,
    );
    output.done();
    f.testwl.dispatch();

    match connection.await_event() {
        xcb::Event::X(x::Event::ConfigureNotify(e)) => {
            assert_eq!(e.window(), connection.root);
            assert_eq!(e.width(), 1000);
            assert_eq!(e.height(), 100);
        }
        other => panic!("unexpected event {other:?}"),
    }
}

const BTN_LEFT: u32 = 0x110;

#[test]
fn client_init_move() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let win_toplevel = connection.new_window(connection.root, 0, 0, 20, 20, false);
    let surface = f.map_as_toplevel(&mut connection, win_toplevel);
    f.testwl.move_pointer_to(surface, 10., 10.);
    let ptr = f.testwl.pointer();
    ptr.motion(10, 10.0, 10.0);
    ptr.frame();
    ptr.button(10, 20, BTN_LEFT, wl_pointer::ButtonState::Pressed);
    ptr.frame();
    f.testwl.dispatch();

    connection.send_client_message(&x::ClientMessageEvent::new(
        win_toplevel,
        connection.atoms.moveresize,
        x::ClientMessageData::Data32([0, 0, MoveResizeDirection::Move.into(), 1, 0]),
    ));

    f.wait_and_dispatch();
    let data = f.testwl.get_surface_data(surface).unwrap();
    assert!(data.moving);
}

#[test]
fn client_init_resize() {
    let mut f = Fixture::new();
    let mut connection = Connection::new(&f.display);

    let win_toplevel = connection.new_window(connection.root, 0, 0, 20, 20, false);
    let surface = f.map_as_toplevel(&mut connection, win_toplevel);
    f.testwl.move_pointer_to(surface, 10., 10.);
    let ptr = f.testwl.pointer();
    ptr.motion(10, 10.0, 10.0);
    ptr.frame();
    ptr.button(10, 20, BTN_LEFT, wl_pointer::ButtonState::Pressed);
    ptr.frame();
    f.testwl.dispatch();

    connection.send_client_message(&x::ClientMessageEvent::new(
        win_toplevel,
        connection.atoms.moveresize,
        x::ClientMessageData::Data32([0, 0, MoveResizeDirection::SizeBottomRight.into(), 1, 0]),
    ));

    f.wait_and_dispatch();
    let data = f.testwl.get_surface_data(surface).unwrap();
    assert!(
        matches!(data.resizing, Some(xdg_toplevel::ResizeEdge::BottomRight)),
        "Got wrong resizing edge: {:?}",
        data.resizing
    );
}
