/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// Currently the mozdevice API is not safe for multiple requests at the same
// time. It is recommended to run each of the unit tests on its own. Also adb
// specific tests cannot be run in CI yet. To check those locally, also run
// the ignored tests.
//
// Use the following command to accomplish that:
//
//     $ cargo test -- --ignored --test-threads=1

use crate::*;

use std::collections::BTreeSet;
use std::panic;
use std::path::PathBuf;
use tempfile::{tempdir, TempDir};

#[test]
fn read_length_from_valid_string() {
    fn test(message: &str) -> Result<usize> {
        read_length(&mut io::BufReader::new(message.as_bytes()))
    }

    assert_eq!(test("0000").unwrap(), 0);
    assert_eq!(test("0001").unwrap(), 1);
    assert_eq!(test("000F").unwrap(), 15);
    assert_eq!(test("00FF").unwrap(), 255);
    assert_eq!(test("0FFF").unwrap(), 4095);
    assert_eq!(test("FFFF").unwrap(), 65535);

    assert_eq!(test("FFFF0").unwrap(), 65535);
}

#[test]
fn read_length_from_invalid_string() {
    fn test(message: &str) -> Result<usize> {
        read_length(&mut io::BufReader::new(message.as_bytes()))
    }

    test("").expect_err("empty string");
    test("G").expect_err("invalid hex character");
    test("-1").expect_err("negative number");
    test("000").expect_err("shorter than 4 bytes");
}

#[test]
fn encode_message_with_valid_string() {
    assert_eq!(encode_message("").unwrap(), "0000".to_string());
    assert_eq!(encode_message("a").unwrap(), "0001a".to_string());
    assert_eq!(
        encode_message(&"a".repeat(15)).unwrap(),
        format!("000F{}", "a".repeat(15))
    );
    assert_eq!(
        encode_message(&"a".repeat(255)).unwrap(),
        format!("00FF{}", "a".repeat(255))
    );
    assert_eq!(
        encode_message(&"a".repeat(4095)).unwrap(),
        format!("0FFF{}", "a".repeat(4095))
    );
    assert_eq!(
        encode_message(&"a".repeat(65535)).unwrap(),
        format!("FFFF{}", "a".repeat(65535))
    );
}

#[test]
fn encode_message_with_invalid_string() {
    encode_message(&"a".repeat(65536)).expect_err("string lengths exceeds 4 bytes");
}

fn run_device_test<F>(test: F)
where
    F: FnOnce(&Device, &TempDir, &UnixPath) + panic::UnwindSafe,
{
    let host = Host {
        ..Default::default()
    };
    let device = host
        .device_or_default::<String>(None, AndroidStorageInput::Auto)
        .expect("device_or_default");

    let tmp_dir = tempdir().expect("create temp dir");
    let response = device
        .execute_host_shell_command("echo $EXTERNAL_STORAGE")
        .unwrap();
    let mut test_root = UnixPathBuf::from(response.trim_end_matches('\n'));
    test_root.push("mozdevice");

    let _ = device.remove(&test_root);

    let result = panic::catch_unwind(|| test(&device, &tmp_dir, &test_root));

    let _ = device.kill_forward_all_ports();
    // let _ = device.kill_reverse_all_ports();

    assert!(result.is_ok())
}

#[test]
#[ignore]
fn host_features() {
    let host = Host {
        ..Default::default()
    };

    let set = host.features::<BTreeSet<_>>().expect("to query features");
    assert!(set.contains("cmd"));
    assert!(set.contains("shell_v2"));
}

#[test]
#[ignore]
fn host_devices() {
    let host = Host {
        ..Default::default()
    };

    let set: BTreeSet<_> = host.devices().expect("to query devices");
    assert_eq!(1, set.len());
}

#[test]
#[ignore]
fn host_device_or_default() {
    let host = Host {
        ..Default::default()
    };

    let devices: Vec<_> = host.devices().expect("to query devices");
    let expected_device = devices.first().expect("found a device");

    let device = host
        .device_or_default::<String>(Some(&expected_device.serial), AndroidStorageInput::App)
        .expect("connected device with serial");
    assert_eq!(device.run_as_package, None);
    assert_eq!(device.serial, expected_device.serial);
    assert!(device.tempfile.starts_with("/data/local/tmp"));
}

#[test]
#[ignore]
fn host_device_or_default_invalid_serial() {
    let host = Host {
        ..Default::default()
    };

    host.device_or_default::<String>(Some(&"foobar".to_owned()), AndroidStorageInput::Auto)
        .expect_err("invalid serial");
}

#[test]
#[ignore]
fn host_device_or_default_no_serial() {
    let host = Host {
        ..Default::default()
    };

    let devices: Vec<_> = host.devices().expect("to query devices");
    let expected_device = devices.first().expect("found a device");

    let device = host
        .device_or_default::<String>(None, AndroidStorageInput::Auto)
        .expect("connected device with serial");
    assert_eq!(device.serial, expected_device.serial);
}

#[test]
#[ignore]
fn host_device_or_default_storage_as_app() {
    let host = Host {
        ..Default::default()
    };

    let device = host
        .device_or_default::<String>(None, AndroidStorageInput::App)
        .expect("connected device");
    assert_eq!(device.storage, AndroidStorage::App);
}

#[test]
#[ignore]
fn host_device_or_default_storage_as_auto() {
    let host = Host {
        ..Default::default()
    };

    let device = host
        .device_or_default::<String>(None, AndroidStorageInput::Auto)
        .expect("connected device");
    assert_eq!(device.storage, AndroidStorage::Sdcard);
}

#[test]
#[ignore]
fn host_device_or_default_storage_as_internal() {
    let host = Host {
        ..Default::default()
    };

    let device = host
        .device_or_default::<String>(None, AndroidStorageInput::Internal)
        .expect("connected device");
    assert_eq!(device.storage, AndroidStorage::Internal);
}

#[test]
#[ignore]
fn host_device_or_default_storage_as_sdcard() {
    let host = Host {
        ..Default::default()
    };

    let device = host
        .device_or_default::<String>(None, AndroidStorageInput::Sdcard)
        .expect("connected device");
    assert_eq!(device.storage, AndroidStorage::Sdcard);
}

#[test]
#[ignore]
fn device_shell_command() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        assert_eq!(
            "Linux\n",
            device
                .execute_host_shell_command("uname")
                .expect("to have shell output")
        );
    });
}

#[test]
#[ignore]
fn device_forward_port_hardcoded() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        assert_eq!(
            3035,
            device
                .forward_port(3035, 3036)
                .expect("forwarded local port")
        );
        // TODO: check with forward --list
    });
}

// #[test]
// #[ignore]
// TODO: "adb server response to `forward tcp:0 ...` was not a u16: \"000559464\"")
// fn device_forward_port_system_allocated() {
//     run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
//         let local_port = device.forward_port(0, 3037).expect("local_port");
//         assert_ne!(local_port, 0);
//         // TODO: check with forward --list
//     });
// }

#[test]
#[ignore]
fn device_kill_forward_port_no_forwarded_port() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        device
            .kill_forward_port(3038)
            .expect_err("adb error: listener 'tcp:3038' ");
    });
}

#[test]
#[ignore]
fn device_kill_forward_port_twice() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        let local_port = device
            .forward_port(3039, 3040)
            .expect("forwarded local port");
        assert_eq!(local_port, 3039);
        // TODO: check with forward --list
        device
            .kill_forward_port(local_port)
            .expect("to remove forwarded port");
        device
            .kill_forward_port(local_port)
            .expect_err("adb error: listener 'tcp:3039' ");
    });
}

#[test]
#[ignore]
fn device_kill_forward_all_ports_no_forwarded_port() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        device
            .kill_forward_all_ports()
            .expect("to not fail for no forwarded ports");
    });
}

#[test]
#[ignore]
fn device_kill_forward_all_ports_twice() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        let local_port1 = device
            .forward_port(3039, 3040)
            .expect("forwarded local port");
        assert_eq!(local_port1, 3039);
        let local_port2 = device
            .forward_port(3041, 3042)
            .expect("forwarded local port");
        assert_eq!(local_port2, 3041);
        // TODO: check with forward --list
        device
            .kill_forward_all_ports()
            .expect("to remove all forwarded ports");
        device
            .kill_forward_all_ports()
            .expect("to not fail for no forwarded ports");
    });
}

#[test]
#[ignore]
fn device_reverse_port_hardcoded() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        assert_eq!(4035, device.reverse_port(4035, 4036).expect("remote_port"));
        // TODO: check with reverse --list
    });
}

// #[test]
// #[ignore]
// TODO: No adb response: ParseInt(ParseIntError { kind: Empty })
// fn device_reverse_port_system_allocated() {
//     run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
//         let reverse_port = device.reverse_port(0, 4037).expect("remote port");
//         assert_ne!(reverse_port, 0);
//         // TODO: check with reverse --list
//     });
// }

#[test]
#[ignore]
fn device_kill_reverse_port_no_reverse_port() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        device
            .kill_reverse_port(4038)
            .expect_err("listener 'tcp:4038' not found");
    });
}

// #[test]
// #[ignore]
// TODO: "adb error: adb server response did not contain expected hexstring length: \"\""
// fn device_kill_reverse_port_twice() {
//     run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
//         let remote_port = device
//             .reverse_port(4039, 4040)
//             .expect("reversed local port");
//         assert_eq!(remote_port, 4039);
//         // TODO: check with reverse --list
//         device
//             .kill_reverse_port(remote_port)
//             .expect("to remove reverse port");
//         device
//             .kill_reverse_port(remote_port)
//             .expect_err("listener 'tcp:4039' not found");
//     });
// }

#[test]
#[ignore]
fn device_kill_reverse_all_ports_no_reversed_port() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        device
            .kill_reverse_all_ports()
            .expect("to not fail for no reversed ports");
    });
}

#[test]
#[ignore]
fn device_kill_reverse_all_ports_twice() {
    run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| {
        let local_port1 = device
            .forward_port(4039, 4040)
            .expect("forwarded local port");
        assert_eq!(local_port1, 4039);
        let local_port2 = device
            .forward_port(4041, 4042)
            .expect("forwarded local port");
        assert_eq!(local_port2, 4041);
        // TODO: check with reverse --list
        device
            .kill_reverse_all_ports()
            .expect("to remove all reversed ports");
        device
            .kill_reverse_all_ports()
            .expect("to not fail for no reversed ports");
    });
}

#[test]
#[ignore]
fn device_push_pull_text_file() {
    run_device_test(
        |device: &Device, _: &TempDir, remote_root_path: &UnixPath| {
            let content = "test";
            let remote_path = remote_root_path.join("foo.txt");

            device
                .push(
                    &mut io::BufReader::new(content.as_bytes()),
                    &remote_path,
                    0o777,
                )
                .expect("file has been pushed");

            let file_content = device
                .execute_host_shell_command(&format!("cat {}", remote_path.display()))
                .expect("host shell command for 'cat' to succeed");

            assert_eq!(file_content, content);

            // And as second step pull it off the device.
            let mut buffer = Vec::new();
            device
                .pull(&remote_path, &mut buffer)
                .expect("file has been pulled");
            assert_eq!(buffer, content.as_bytes());
        },
    );
}

#[test]
#[ignore]
fn device_push_pull_large_binary_file() {
    run_device_test(
        |device: &Device, _: &TempDir, remote_root_path: &UnixPath| {
            let remote_path = remote_root_path.join("foo.binary");

            let mut content = Vec::new();

            // Needs to be larger than 64kB to test multiple chunks.
            for i in 0..100000u32 {
                content.push('0' as u8 + (i % 10) as u8);
            }

            device
                .push(
                    &mut std::io::Cursor::new(content.clone()),
                    &remote_path,
                    0o777,
                )
                .expect("large file has been pushed");

            let output = device
                .execute_host_shell_command(&format!("ls -l {}", remote_path.display()))
                .expect("host shell command for 'ls' to succeed");

            assert!(output.contains(remote_path.to_str().unwrap()));

            let mut buffer = Vec::new();

            device
                .pull(&remote_path, &mut buffer)
                .expect("large binary file has been pulled");
            assert_eq!(buffer, content);
        },
    );
}

#[test]
#[ignore]
fn device_push_permission() {
    run_device_test(
        |device: &Device, _: &TempDir, remote_root_path: &UnixPath| {
            fn adjust_mode(mode: u32) -> u32 {
                // Adjust the mode by copying the user permissions to
                // group and other as indicated in
                // [send_impl](https://android.googlesource.com/platform/system/core/+/master/adb/daemon/file_sync_service.cpp#516).
                // This ensures that group and other can both access a
                // file if the user can access it.
                let mut m = mode & 0o777;
                m |= (m >> 3) & 0o070;
                m |= (m >> 3) & 0o007;
                m
            }

            fn get_permissions(mode: u32) -> String {
                // Convert the mode integer into the string representation
                // of the mode returned by `ls`. This assumes the object is
                // a file and not a directory.
                let mut perms = vec!["-", "r", "w", "x", "r", "w", "x", "r", "w", "x"];
                let mut bit_pos = 0;
                while bit_pos < 9 {
                    if (1 << bit_pos) & mode == 0 {
                        perms[9 - bit_pos] = "-"
                    }
                    bit_pos += 1;
                }
                perms.concat()
            }
            let content = "test";
            let remote_path = remote_root_path.join("foo.bar");

            // First push the file to the device
            let modes = vec![0o421, 0o644, 0o666, 0o777];
            for mode in modes {
                let adjusted_mode = adjust_mode(mode);
                let adjusted_perms = get_permissions(adjusted_mode);
                device
                    .push(
                        &mut io::BufReader::new(content.as_bytes()),
                        &remote_path,
                        mode,
                    )
                    .expect("file has been pushed");

                let output = device
                    .execute_host_shell_command(&format!("ls -l {}", remote_path.display()))
                    .expect("host shell command for 'ls' to succeed");

                assert!(output.contains(remote_path.to_str().unwrap()));
                assert!(output.starts_with(&adjusted_perms));
            }

            let output = device
                .execute_host_shell_command(&format!("ls -ld {}", remote_root_path.display()))
                .expect("host shell command for 'ls parent' to succeed");

            assert!(output.contains(remote_root_path.to_str().unwrap()));
            assert!(output.starts_with("drwxrwxrwx"));
        },
    );
}

#[test]
#[ignore]
fn device_pull_fails_for_missing_file() {
    run_device_test(
        |device: &Device, _: &TempDir, remote_root_path: &UnixPath| {
            let mut buffer = Vec::new();

            device
                .pull(&remote_root_path.join("missing"), &mut buffer)
                .expect_err("missing file should not be pulled");
        },
    );
}

#[test]
#[ignore]
fn device_push_and_list_dir() {
    run_device_test(
        |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| {
            let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"];

            for file in files.iter() {
                let path = tmp_dir.path().join(Path::new(file));
                let _ = std::fs::create_dir_all(path.parent().unwrap());

                let f = File::create(path).expect("to create file");
                let mut f = io::BufWriter::new(f);
                f.write_all(file.as_bytes()).expect("to write data");
            }

            device
                .push_dir(tmp_dir.path(), &remote_root_path, 0o777)
                .expect("to push_dir");

            for file in files.iter() {
                let path = append_components(remote_root_path, Path::new(file)).unwrap();
                let output = device
                    .execute_host_shell_command(&format!("ls {}", path.display()))
                    .expect("host shell command for 'ls' to succeed");

                assert!(output.contains(path.to_str().unwrap()));
            }

            let mut listings = device.list_dir(&remote_root_path).expect("to list_dir");
            listings.sort();
            assert_eq!(
                listings,
                vec![
                    RemoteDirEntry {
                        depth: 0,
                        name: "foo1.bar".to_string(),
                        metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
                            mode: 0b110110000,
                            size: 8
                        })
                    },
                    RemoteDirEntry {
                        depth: 0,
                        name: "foo2.bar".to_string(),
                        metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
                            mode: 0b110110000,
                            size: 8
                        })
                    },
                    RemoteDirEntry {
                        depth: 0,
                        name: "bar".to_string(),
                        metadata: RemoteMetadata::RemoteDir
                    },
                    RemoteDirEntry {
                        depth: 1,
                        name: "bar/foo3.bar".to_string(),
                        metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
                            mode: 0b110110000,
                            size: 12
                        })
                    },
                    RemoteDirEntry {
                        depth: 1,
                        name: "bar/more".to_string(),
                        metadata: RemoteMetadata::RemoteDir
                    },
                    RemoteDirEntry {
                        depth: 2,
                        name: "bar/more/foo3.bar".to_string(),
                        metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
                            mode: 0b110110000,
                            size: 17
                        })
                    }
                ]
            );
        },
    );
}

#[test]
#[ignore]
fn device_push_and_pull_dir() {
    run_device_test(
        |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| {
            let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"];

            let src_dir = tmp_dir.path().join(Path::new("src"));
            let dest_dir = tmp_dir.path().join(Path::new("src"));

            for file in files.iter() {
                let path = src_dir.join(Path::new(file));
                let _ = std::fs::create_dir_all(path.parent().unwrap());

                let f = File::create(path).expect("to create file");
                let mut f = io::BufWriter::new(f);
                f.write_all(file.as_bytes()).expect("to write data");
            }

            device
                .push_dir(&src_dir, &remote_root_path, 0o777)
                .expect("to push_dir");

            device
                .pull_dir(remote_root_path, &dest_dir)
                .expect("to pull_dir");

            for file in files.iter() {
                let path = dest_dir.join(Path::new(file));
                let mut f = File::open(path).expect("to open file");
                let mut buf = String::new();
                f.read_to_string(&mut buf).expect("to read content");
                assert_eq!(buf, *file);
            }
        },
    )
}

#[test]
#[ignore]
fn device_push_and_list_dir_flat() {
    run_device_test(
        |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| {
            let content = "test";

            let files = [
                PathBuf::from("foo1.bar"),
                PathBuf::from("foo2.bar"),
                PathBuf::from("bar").join("foo3.bar"),
            ];

            for file in files.iter() {
                let path = tmp_dir.path().join(&file);
                let _ = std::fs::create_dir_all(path.parent().unwrap());

                let f = File::create(path).expect("to create file");
                let mut f = io::BufWriter::new(f);
                f.write_all(content.as_bytes()).expect("to write data");
            }

            device
                .push_dir(tmp_dir.path(), &remote_root_path, 0o777)
                .expect("to push_dir");

            for file in files.iter() {
                let path = append_components(remote_root_path, file).unwrap();
                let output = device
                    .execute_host_shell_command(&format!("ls {}", path.display()))
                    .expect("host shell command for 'ls' to succeed");

                assert!(output.contains(path.to_str().unwrap()));
            }

            let mut listings = device
                .list_dir_flat(&remote_root_path, 7, "prefix".to_string())
                .expect("to list_dir_flat");
            listings.sort();
            assert_eq!(
                listings,
                vec![
                    RemoteDirEntry {
                        depth: 7,
                        metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
                            mode: 0b110110000,
                            size: 4
                        }),
                        name: "prefix/foo1.bar".to_string(),
                    },
                    RemoteDirEntry {
                        depth: 7,
                        metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata {
                            mode: 0b110110000,
                            size: 4
                        }),
                        name: "prefix/foo2.bar".to_string(),
                    },
                    RemoteDirEntry {
                        depth: 7,
                        metadata: RemoteMetadata::RemoteDir,
                        name: "prefix/bar".to_string(),
                    },
                ]
            );
        },
    );
}

#[test]
fn format_own_device_error_types() {
    assert_eq!(
        format!("{}", DeviceError::InvalidStorage),
        "Invalid storage".to_string()
    );
    assert_eq!(
        format!("{}", DeviceError::MissingPackage),
        "Missing package".to_string()
    );
    assert_eq!(
        format!("{}", DeviceError::MultipleDevices),
        "Multiple Android devices online".to_string()
    );

    assert_eq!(
        format!("{}", DeviceError::Adb("foo".to_string())),
        "foo".to_string()
    );
}
