use super::*;
use crate::{ArgCmp, Syscall, ViolationAction};

const LD: u16 = BPF_LD | BPF_W | BPF_ABS;
const JEQ: u16 = BPF_JMP | BPF_JEQ | BPF_K;
const JGT: u16 = BPF_JMP | BPF_JGT | BPF_K;
const JGE: u16 = BPF_JMP | BPF_JGE | BPF_K;
const AND: u16 = BPF_ALU | BPF_AND | BPF_K;
const RET: u16 = BPF_RET | BPF_K;
const JA: u16 = BPF_JMP | BPF_JA;
const VALUE: u64 = 0x1122_3344_5566_7788;
const MASK: u64 = 0xff00_ff00_ff00_ff00;
const MSB: u32 = 0x1122_3344;
const LSB: u32 = 0x5566_7788;

fn fields(instruction: &sock_filter) -> (u16, u8, u8, u32) {
    (
        instruction.code,
        instruction.jt,
        instruction.jf,
        instruction.k,
    )
}

fn allow(syscall: Syscall) -> FilterRule {
    FilterRule::new(syscall, Vec::new(), RuleAction::Allow)
}

fn allow_read_if(args: Vec<ArgCmp>) -> FilterRule {
    FilterRule::new(Syscall::read, args, RuleAction::Allow)
}

fn action_code(action: SeccompAction) -> u32 {
    action.into()
}

fn condition_slice(cmp: ArgCmp) -> Vec<(u16, u8, u8, u32)> {
    let rules = vec![allow_read_if(vec![cmp])];
    let program = compile(rules, ViolationAction::KillProcess).unwrap();
    program[6..program.len() - 2].iter().map(fields).collect()
}

fn equal_args(count: usize) -> Vec<ArgCmp> {
    let cmp = ArgCmp::Equal {
        arg_idx: 0,
        value: VALUE.into(),
    };
    vec![cmp; count]
}

fn all_operators() -> Vec<ArgCmp> {
    let value = VALUE.into();
    vec![
        ArgCmp::NotEqual { arg_idx: 0, value },
        ArgCmp::LessThan { arg_idx: 0, value },
        ArgCmp::LessThanOrEqual { arg_idx: 0, value },
        ArgCmp::Equal { arg_idx: 0, value },
        ArgCmp::GreaterThanOrEqual { arg_idx: 0, value },
        ArgCmp::GreaterThan { arg_idx: 0, value },
        ArgCmp::EqualMasked {
            arg_idx: 0,
            mask: MASK.into(),
            value,
        },
    ]
}

#[test]
fn compile_with_no_rules_emits_arch_check_and_violation_return() {
    let program = compile(Vec::new(), ViolationAction::KillProcess).unwrap();
    let kill = action_code(SeccompAction::KillProcess);
    assert_eq!(program.len(), 4);
    assert_eq!(fields(&program[0]), (LD, 0, 0, SECCOMP_DATA_ARCH_OFFSET));
    assert_eq!(fields(&program[1]), (JEQ, 1, 0, audit_arch().unwrap()));
    assert_eq!(fields(&program[2]), (RET, 0, 0, kill));
    assert_eq!(fields(&program[3]), (RET, 0, 0, kill));
}

#[test]
fn allow_and_log_violation_sets_log_return() {
    let program = compile(Vec::new(), ViolationAction::AllowAndLog).unwrap();
    let last = program.last().unwrap();
    assert_eq!(fields(last), (RET, 0, 0, action_code(SeccompAction::Log)));
}

#[test]
fn single_rule_gates_on_syscall_number_and_skips_body() {
    let rules = vec![allow(Syscall::read)];
    let program = compile(rules, ViolationAction::KillProcess).unwrap();
    let number = Syscall::read.number() as u32;
    assert_eq!(program.len(), 8);
    assert_eq!(fields(&program[3]), (LD, 0, 0, SECCOMP_DATA_NR_OFFSET));
    assert_eq!(fields(&program[4]), (JEQ, 1, 0, number));
    assert_eq!(fields(&program[5]), (JA, 0, 0, 1));
    let allow_ret = action_code(SeccompAction::Allow);
    assert_eq!(fields(&program[6]), (RET, 0, 0, allow_ret));
}

#[test]
fn multiple_rules_order_error_then_log_then_allow() {
    let rules = vec![
        allow(Syscall::read),
        FilterRule::new(Syscall::write, Vec::new(), RuleAction::Log),
        FilterRule::new(Syscall::close, Vec::new(), RuleAction::Error(5)),
    ];
    let program = compile(rules, ViolationAction::KillProcess).unwrap();
    let returns: Vec<u32> = program[3..program.len() - 1]
        .iter()
        .filter(|instruction| instruction.code == RET)
        .map(|instruction| instruction.k)
        .collect();
    let expected = vec![
        action_code(SeccompAction::Errno(5)),
        action_code(SeccompAction::Log),
        action_code(SeccompAction::Allow),
    ];
    assert_eq!(returns, expected);
}

#[test]
fn composed_rule_lists_gate_every_syscall() {
    let client = vec![allow(Syscall::socket), allow(Syscall::connect)];
    let mut server = client;
    server.extend(vec![allow(Syscall::listen)]);
    let program = compile(server, ViolationAction::KillProcess).unwrap();
    let gates: Vec<u32> = program[3..]
        .iter()
        .filter(|instruction| instruction.code == JEQ && instruction.jt == 1)
        .map(|instruction| instruction.k)
        .collect();
    let expected: Vec<u32> = [Syscall::socket, Syscall::connect, Syscall::listen]
        .iter()
        .map(|syscall| syscall.number() as u32)
        .collect();
    assert_eq!(gates, expected);
}

#[test]
fn equal_condition_checks_both_argument_words() {
    let cmp = ArgCmp::Equal {
        arg_idx: 1,
        value: VALUE.into(),
    };
    let expected = vec![
        (LD, 0, 0, 28),
        (JEQ, 0, 3, MSB),
        (LD, 0, 0, 24),
        (JEQ, 0, 1, LSB),
    ];
    assert_eq!(condition_slice(cmp), expected);
}

#[test]
fn not_equal_condition_jumps_away_on_full_match() {
    let cmp = ArgCmp::NotEqual {
        arg_idx: 1,
        value: VALUE.into(),
    };
    let expected = vec![
        (LD, 0, 0, 28),
        (JEQ, 0, 2, MSB),
        (LD, 0, 0, 24),
        (JEQ, 1, 0, LSB),
    ];
    assert_eq!(condition_slice(cmp), expected);
}

#[test]
fn greater_or_equal_condition_passes_on_msb_above_value() {
    let cmp = ArgCmp::GreaterThanOrEqual {
        arg_idx: 1,
        value: VALUE.into(),
    };
    let expected = vec![
        (LD, 0, 0, 28),
        (JGT, 3, 0, MSB),
        (JEQ, 0, 3, MSB),
        (LD, 0, 0, 24),
        (JGE, 0, 1, LSB),
    ];
    assert_eq!(condition_slice(cmp), expected);
}

#[test]
fn greater_than_condition_requires_strict_lsb_compare() {
    let cmp = ArgCmp::GreaterThan {
        arg_idx: 1,
        value: VALUE.into(),
    };
    let expected = vec![
        (LD, 0, 0, 28),
        (JGT, 3, 0, MSB),
        (JEQ, 0, 3, MSB),
        (LD, 0, 0, 24),
        (JGT, 0, 1, LSB),
    ];
    assert_eq!(condition_slice(cmp), expected);
}

#[test]
fn less_or_equal_condition_jumps_away_on_msb_above_value() {
    let cmp = ArgCmp::LessThanOrEqual {
        arg_idx: 1,
        value: VALUE.into(),
    };
    let expected = vec![
        (LD, 0, 0, 28),
        (JGT, 4, 0, MSB),
        (JEQ, 0, 2, MSB),
        (LD, 0, 0, 24),
        (JGT, 1, 0, LSB),
    ];
    assert_eq!(condition_slice(cmp), expected);
}

#[test]
fn less_than_condition_jumps_away_on_lsb_at_value() {
    let cmp = ArgCmp::LessThan {
        arg_idx: 1,
        value: VALUE.into(),
    };
    let expected = vec![
        (LD, 0, 0, 28),
        (JGT, 4, 0, MSB),
        (JEQ, 0, 2, MSB),
        (LD, 0, 0, 24),
        (JGE, 1, 0, LSB),
    ];
    assert_eq!(condition_slice(cmp), expected);
}

#[test]
fn masked_equal_condition_masks_each_word_before_compare() {
    let cmp = ArgCmp::EqualMasked {
        arg_idx: 1,
        mask: MASK.into(),
        value: VALUE.into(),
    };
    let expected = vec![
        (LD, 0, 0, 28),
        (AND, 0, 0, 0xff00_ff00),
        (JEQ, 0, 4, 0x1100_3300),
        (LD, 0, 0, 24),
        (AND, 0, 0, 0xff00_ff00),
        (JEQ, 0, 1, 0x5500_7700),
    ];
    assert_eq!(condition_slice(cmp), expected);
}

#[test]
fn condition_len_matches_generated_instruction_count() {
    for cmp in all_operators() {
        assert_eq!(condition_slice(cmp).len(), condition_len(cmp));
    }
}

#[test]
fn chained_conditions_jump_past_rule_body_on_failure() {
    let rules = vec![allow_read_if(equal_args(2))];
    let program = compile(rules, ViolationAction::KillProcess).unwrap();
    let body: Vec<_> = program[6..program.len() - 2].iter().map(fields).collect();
    assert_eq!(body[1], (JEQ, 0, 7, MSB));
    assert_eq!(body[3], (JEQ, 0, 5, LSB));
    assert_eq!(body[5], (JEQ, 0, 3, MSB));
    assert_eq!(body[7], (JEQ, 0, 1, LSB));
}

#[test]
fn arg_index_five_is_accepted() {
    let cmp = ArgCmp::Equal {
        arg_idx: 5,
        value: VALUE.into(),
    };
    let body = condition_slice(cmp);
    assert_eq!(body[0], (LD, 0, 0, 60));
    assert_eq!(body[2], (LD, 0, 0, 56));
}

#[test]
fn arg_index_six_is_rejected() {
    let cmp = ArgCmp::Equal {
        arg_idx: 6,
        value: VALUE.into(),
    };
    let rules = vec![allow_read_if(vec![cmp])];
    let error = compile(rules, ViolationAction::KillProcess).unwrap_err();
    assert_eq!(error.reason(), "invalid syscall argument index 6");
}

#[test]
fn condition_chain_at_jump_offset_limit_compiles() {
    let rules = vec![allow_read_if(equal_args(63))];
    let program = compile(rules, ViolationAction::KillProcess).unwrap();
    assert_eq!(fields(&program[7]), (JEQ, 0, 251, MSB));
    let last_condition_jump = program.len() - 3;
    assert_eq!(fields(&program[last_condition_jump]), (JEQ, 0, 1, LSB));
}

#[test]
fn condition_chain_past_jump_offset_limit_errors() {
    let rules = vec![allow_read_if(equal_args(64))];
    let error = compile(rules, ViolationAction::KillProcess).unwrap_err();
    assert_eq!(error.reason(), "seccomp filter has 257 BPF instructions");
}

#[test]
fn program_under_max_len_compiles() {
    let rules: Vec<FilterRule> = (0..1022).map(|n| allow(Syscall::from_number(n))).collect();
    let program = compile(rules, ViolationAction::KillProcess).unwrap();
    assert_eq!(program.len(), 4092);
}

#[test]
fn program_at_max_len_errors() {
    let rules: Vec<FilterRule> = (0..1023).map(|n| allow(Syscall::from_number(n))).collect();
    assert!(compile(rules, ViolationAction::KillProcess).is_err());
}

#[test]
fn negative_syscall_number_is_rejected() {
    let rules = vec![allow(Syscall::from_number(-1))];
    let error = compile(rules, ViolationAction::KillProcess).unwrap_err();
    assert_eq!(error.reason(), "invalid Linux syscall number -1");
}
