// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
// SPDX-FileCopyrightText: 2022 Linaro Ltd.
// SPDX-FileCopyrightText: 2022 Viresh Kumar <viresh.kumar@linaro.org>

mod common;

mod line_request {
    use libc::{E2BIG, EINVAL};
    use std::time::Duration;

    use crate::common::*;
    use gpiosim_sys::{Pull, Value as SimValue};
    use libgpiod::{
        Error as ChipError, OperationType,
        line::{
            self, Bias, Direction, Drive, Edge, EventClock, Offset, SettingVal, Value, ValueMap,
        },
    };

    const NGPIO: usize = 8;

    mod invalid_arguments {
        use super::*;

        #[test]
        fn no_offsets() {
            let mut config = TestConfig::new(NGPIO).unwrap();

            assert_eq!(
                config.request_lines().unwrap_err(),
                ChipError::OperationFailed(OperationType::ChipRequestLines, errno::Errno(EINVAL))
            );
        }

        #[test]
        fn out_of_bound_offsets() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[2, 0, 8, 4]);

            assert_eq!(
                config.request_lines().unwrap_err(),
                ChipError::OperationFailed(OperationType::ChipRequestLines, errno::Errno(EINVAL))
            );
        }

        #[test]
        fn dir_out_edge_failure() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_edge(Some(Direction::Output), Some(Edge::Both));
            config.lconfig_add_settings(&[0]);

            assert_eq!(
                config.request_lines().unwrap_err(),
                ChipError::OperationFailed(OperationType::ChipRequestLines, errno::Errno(EINVAL))
            );
        }
    }

    mod verify {
        use super::*;

        #[test]
        #[cfg(feature = "v2_1")]
        fn chip_name() {
            const GPIO: Offset = 2;
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[GPIO]);
            config.request_lines().unwrap();

            let arc = config.sim();
            let sim = arc.lock().unwrap();
            let chip_name = sim.chip_name();

            assert_eq!(config.request().chip_name().unwrap(), chip_name);
        }

        #[test]
        fn custom_consumer() {
            const GPIO: Offset = 2;
            const CONSUMER: &str = "foobar";
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.rconfig_set_consumer(CONSUMER);
            config.lconfig_add_settings(&[GPIO]);
            config.request_lines().unwrap();

            let info = config.chip().line_info(GPIO).unwrap();

            assert!(info.is_used());
            assert_eq!(info.consumer().unwrap(), CONSUMER);
        }

        #[test]
        fn empty_consumer() {
            const GPIO: Offset = 2;
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[GPIO]);
            config.request_lines().unwrap();

            let info = config.chip().line_info(GPIO).unwrap();

            assert!(info.is_used());
            assert_eq!(info.consumer().unwrap(), "?");
        }

        #[test]
        fn read_values() {
            let offsets = [7, 1, 0, 6, 2];
            let pulls = [Pull::Up, Pull::Up, Pull::Down, Pull::Up, Pull::Down];
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.set_pull(&offsets, &pulls);
            config.lconfig_val(Some(Direction::Input), None);
            config.lconfig_add_settings(&offsets);
            config.request_lines().unwrap();

            let request = config.request();

            // Single values read properly
            assert_eq!(request.value(7).unwrap(), Value::Active);

            // Values read properly
            let map = request.values().unwrap();
            for i in 0..offsets.len() {
                assert_eq!(
                    *map.get(offsets[i].into()).unwrap(),
                    match pulls[i] {
                        Pull::Down => Value::InActive,
                        _ => Value::Active,
                    }
                );
            }

            // Subset of values read properly
            let map = request.values_subset(&[2, 0, 6]).unwrap();
            assert_eq!(*map.get(2).unwrap(), Value::InActive);
            assert_eq!(*map.get(0).unwrap(), Value::InActive);
            assert_eq!(*map.get(6).unwrap(), Value::Active);

            // Value read properly after reconfigure
            let mut lsettings = line::Settings::new().unwrap();
            lsettings.set_active_low(true);
            lsettings.set_direction(Direction::Input).unwrap();
            let mut lconfig = line::Config::new().unwrap();
            lconfig.add_line_settings(&offsets, lsettings).unwrap();
            request.reconfigure_lines(&lconfig).unwrap();
            assert_eq!(request.value(7).unwrap(), Value::InActive);
        }

        #[test]
        fn set_output_values() {
            let offsets = [0, 1, 3, 4];
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_val(Some(Direction::Output), Some(Value::Active));
            config.lconfig_add_settings(&offsets);
            config.request_lines().unwrap();

            assert_eq!(config.sim_val(0).unwrap(), SimValue::Active);
            assert_eq!(config.sim_val(1).unwrap(), SimValue::Active);
            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
            assert_eq!(config.sim_val(4).unwrap(), SimValue::Active);

            // Default
            assert_eq!(config.sim_val(2).unwrap(), SimValue::InActive);
        }

        #[test]
        fn update_output_values() {
            let offsets = [0, 1, 3, 4];
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_val(Some(Direction::Output), Some(Value::InActive));
            config.lconfig_add_settings(&offsets);
            config.request_lines().unwrap();

            // Set single value
            config.request().set_value(1, Value::Active).unwrap();
            assert_eq!(config.sim_val(0).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(1).unwrap(), SimValue::Active);
            assert_eq!(config.sim_val(3).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);
            config.request().set_value(1, Value::InActive).unwrap();
            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);

            // Set values of subset
            let mut map = ValueMap::new();
            map.insert(4, Value::Active);
            map.insert(3, Value::Active);
            config.request().set_values_subset(map).unwrap();
            assert_eq!(config.sim_val(0).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
            assert_eq!(config.sim_val(4).unwrap(), SimValue::Active);

            let mut map = ValueMap::new();
            map.insert(4, Value::InActive);
            map.insert(3, Value::InActive);
            config.request().set_values_subset(map).unwrap();
            assert_eq!(config.sim_val(3).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);

            // Set all values
            config
                .request()
                .set_values(&[
                    Value::Active,
                    Value::InActive,
                    Value::Active,
                    Value::InActive,
                ])
                .unwrap();
            assert_eq!(config.sim_val(0).unwrap(), SimValue::Active);
            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);
            config
                .request()
                .set_values(&[
                    Value::InActive,
                    Value::InActive,
                    Value::InActive,
                    Value::InActive,
                ])
                .unwrap();
            assert_eq!(config.sim_val(0).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(1).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(3).unwrap(), SimValue::InActive);
            assert_eq!(config.sim_val(4).unwrap(), SimValue::InActive);
        }

        #[test]
        fn set_bias() {
            let offsets = [3];
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_bias(Direction::Input, Some(Bias::PullUp));
            config.lconfig_add_settings(&offsets);
            config.request_lines().unwrap();
            config.request();

            // Set single value
            assert_eq!(config.sim_val(3).unwrap(), SimValue::Active);
        }

        #[test]
        fn no_events() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_edge(None, Some(Edge::Both));
            config.lconfig_add_settings(&[0]);
            config.request_lines().unwrap();

            // No events available
            assert!(
                !config
                    .request()
                    .wait_edge_events(Some(Duration::from_millis(100)))
                    .unwrap()
            );
        }
    }

    mod reconfigure {
        use super::*;

        #[test]
        fn e2big() {
            let offsets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
            let mut config = TestConfig::new(16).unwrap();
            config.lconfig_add_settings(&offsets);
            config.request_lines().unwrap();

            let request = config.request();

            // Reconfigure
            let mut lsettings = line::Settings::new().unwrap();
            lsettings.set_direction(Direction::Input).unwrap();
            let mut lconfig = line::Config::new().unwrap();

            // The uAPI config has only 10 attribute slots, this should pass.
            for offset in offsets {
                lsettings.set_debounce_period(Duration::from_millis((100 + offset).into()));
                lconfig
                    .add_line_settings(&[offset as Offset], lsettings.try_clone().unwrap())
                    .unwrap();
            }

            assert!(request.reconfigure_lines(&lconfig).is_ok());

            // The uAPI config has only 10 attribute slots, and this is the 11th entry.
            // This should fail with E2BIG.
            lsettings.set_debounce_period(Duration::from_millis(100 + 11));
            lconfig.add_line_settings(&[11], lsettings).unwrap();

            assert_eq!(
                request.reconfigure_lines(&lconfig).unwrap_err(),
                ChipError::OperationFailed(
                    OperationType::LineRequestReconfigLines,
                    errno::Errno(E2BIG),
                )
            );
        }

        #[test]
        fn bias() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[0]);
            config.request_lines().unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.bias().unwrap(), None);

            // Reconfigure
            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::Bias(Some(Bias::PullUp)),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.bias().unwrap(), Some(Bias::PullUp));

            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::Bias(Some(Bias::PullDown)),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.bias().unwrap(), Some(Bias::PullDown));

            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::Bias(Some(Bias::Disabled)),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.bias().unwrap(), Some(Bias::Disabled));
        }

        #[test]
        fn drive() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[0]);
            config.request_lines().unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.drive().unwrap(), Drive::PushPull);

            // Reconfigure
            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::Drive(Drive::PushPull),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.drive().unwrap(), Drive::PushPull);

            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Output),
                    SettingVal::Drive(Drive::OpenDrain),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.drive().unwrap(), Drive::OpenDrain);

            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Output),
                    SettingVal::Drive(Drive::OpenSource),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.drive().unwrap(), Drive::OpenSource);
        }

        #[test]
        fn edge() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[0]);
            config.request_lines().unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.edge_detection().unwrap(), None);

            // Reconfigure
            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::EdgeDetection(Some(Edge::Both)),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Both));

            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::EdgeDetection(Some(Edge::Rising)),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Rising));

            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::EdgeDetection(Some(Edge::Falling)),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.edge_detection().unwrap(), Some(Edge::Falling));
        }

        #[test]
        fn event_clock() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[0]);
            config.request_lines().unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);

            // Reconfigure
            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings.set_event_clock(EventClock::Monotonic).unwrap();
            lsettings.set_direction(Direction::Input).unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);

            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings.set_event_clock(EventClock::Realtime).unwrap();
            lsettings.set_direction(Direction::Input).unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            config.request().reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.event_clock().unwrap(), EventClock::Realtime);
        }

        #[test]
        #[ignore]
        fn event_clock_hte() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[0]);
            config.request_lines().unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.event_clock().unwrap(), EventClock::Monotonic);

            let request = config.request();

            // Reconfigure
            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings.set_event_clock(EventClock::HTE).unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            request.reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert_eq!(info.event_clock().unwrap(), EventClock::HTE);
        }

        #[test]
        fn debounce() {
            let mut config = TestConfig::new(NGPIO).unwrap();
            config.lconfig_add_settings(&[0]);
            config.request_lines().unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert!(!info.is_debounced());
            assert_eq!(info.debounce_period(), Duration::from_millis(0));

            let request = config.request();

            // Reconfigure
            let mut lconfig = line::Config::new().unwrap();
            let mut lsettings = line::Settings::new().unwrap();
            lsettings
                .set_prop(&[
                    SettingVal::Direction(Direction::Input),
                    SettingVal::DebouncePeriod(Duration::from_millis(100)),
                ])
                .unwrap();
            lconfig.add_line_settings(&[0], lsettings).unwrap();
            request.reconfigure_lines(&lconfig).unwrap();
            let info = config.chip().line_info(0).unwrap();
            assert!(info.is_debounced());
            assert_eq!(info.debounce_period(), Duration::from_millis(100));
        }
    }
}
