use crate::internal::as2_pcode::lexer::tokens::{ActionName, Token, TokenKind};
use crate::internal::as2_pcode::parser::Tokens;
use crate::internal::as2_pcode::pcode::{Action, Actions, CatchTarget, PushValue};
use winnow::combinator::{alt, fail, opt, peek, separated};
use winnow::error::{ContextError, ErrMode, ParserError, StrContext, StrContextValue};
use winnow::stream::Stream;
use winnow::token::{any, take_while};
use winnow::{ModalResult, Parser};

pub(crate) fn actions<'i>(
    is_block: bool,
) -> impl Parser<Tokens<'i>, Actions, ErrMode<ContextError>> {
    move |i: &mut Tokens<'i>| {
        let mut actions = Actions::empty();

        take_while(0.., TokenKind::Newline).parse_next(i)?;

        while !i.is_empty() {
            let next = peek(any).parse_next(i)?;

            if next.kind == TokenKind::CloseBrace && is_block {
                break;
            }
            if next.kind == TokenKind::Identifier {
                let identifier = label.parse_next(i)?;
                TokenKind::Colon.parse_next(i)?;
                actions.push_label(identifier);
                take_while(0.., TokenKind::Newline).parse_next(i)?;
                continue;
            }
            actions.push(action.parse_next(i)?);
            if !i.is_empty() {
                take_while(1.., TokenKind::Newline).parse_next(i)?;
            }
        }

        Ok(actions)
    }
}

pub(crate) fn action(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let Token {
        kind: TokenKind::ActionName(name),
        ..
    } = any.parse_next(i)?
    else {
        return fail.context(StrContext::Label("action name")).parse_next(i);
    };

    Ok(match name {
        ActionName::Add => Action::Add,
        ActionName::Add2 => Action::Add2,
        ActionName::And => Action::And,
        ActionName::AsciiToChar => Action::AsciiToChar,
        ActionName::BitAnd => Action::BitAnd,
        ActionName::BitLShift => Action::BitLShift,
        ActionName::BitOr => Action::BitOr,
        ActionName::BitRShift => Action::BitRShift,
        ActionName::BitURShift => Action::BitURShift,
        ActionName::BitXor => Action::BitXor,
        ActionName::Call => Action::Call,
        ActionName::CallFunction => Action::CallFunction,
        ActionName::CallMethod => Action::CallMethod,
        ActionName::CastOp => Action::CastOp,
        ActionName::CharToAscii => Action::CharToAscii,
        ActionName::CloneSprite => Action::CloneSprite,
        ActionName::ConstantPool => constant_pool.parse_next(i)?,
        ActionName::Decrement => Action::Decrement,
        ActionName::DefineFunction => define_function.parse_next(i)?,
        ActionName::DefineLocal => Action::DefineLocal,
        ActionName::DefineLocal2 => Action::DefineLocal2,
        ActionName::Delete => Action::Delete,
        ActionName::Delete2 => Action::Delete2,
        ActionName::Divide => Action::Divide,
        ActionName::End => Action::End,
        ActionName::EndDrag => Action::EndDrag,
        ActionName::Enumerate => Action::Enumerate,
        ActionName::Enumerate2 => Action::Enumerate2,
        ActionName::Equals => Action::Equals,
        ActionName::Equals2 => Action::Equals2,
        ActionName::Extends => Action::Extends,
        ActionName::GetMember => Action::GetMember,
        ActionName::GetProperty => Action::GetProperty,
        ActionName::GetTime => Action::GetTime,
        ActionName::GetUrl => get_url.parse_next(i)?,
        ActionName::GetUrl2 => get_url_2.parse_next(i)?,
        ActionName::GetVariable => Action::GetVariable,
        ActionName::GotoFrame => goto_frame.parse_next(i)?,
        ActionName::GotoFrame2 => goto_frame_2.parse_next(i)?,
        ActionName::GotoLabel => goto_label.parse_next(i)?,
        ActionName::Greater => Action::Greater,
        ActionName::If => if_.parse_next(i)?,
        ActionName::ImplementsOp => Action::ImplementsOp,
        ActionName::Increment => Action::Increment,
        ActionName::InitArray => Action::InitArray,
        ActionName::InitObject => Action::InitObject,
        ActionName::InstanceOf => Action::InstanceOf,
        ActionName::Jump => jump.parse_next(i)?,
        ActionName::Less => Action::Less,
        ActionName::Less2 => Action::Less2,
        ActionName::MBAsciiToChar => Action::MBAsciiToChar,
        ActionName::MBCharToAscii => Action::MBCharToAscii,
        ActionName::MBStringExtract => Action::MBStringExtract,
        ActionName::MBStringLength => Action::MBStringLength,
        ActionName::Modulo => Action::Modulo,
        ActionName::Multiply => Action::Multiply,
        ActionName::NewMethod => Action::NewMethod,
        ActionName::NewObject => Action::NewObject,
        ActionName::NextFrame => Action::NextFrame,
        ActionName::Not => Action::Not,
        ActionName::Or => Action::Or,
        ActionName::Play => Action::Play,
        ActionName::Pop => Action::Pop,
        ActionName::PrevFrame => Action::PrevFrame,
        ActionName::Push => push.parse_next(i)?,
        ActionName::PushDuplicate => Action::PushDuplicate,
        ActionName::RandomNumber => Action::RandomNumber,
        ActionName::RemoveSprite => Action::RemoveSprite,
        ActionName::Return => Action::Return,
        ActionName::SetMember => Action::SetMember,
        ActionName::SetProperty => Action::SetProperty,
        ActionName::SetTarget => set_target.parse_next(i)?,
        ActionName::SetTarget2 => Action::SetTarget2,
        ActionName::SetVariable => Action::SetVariable,
        ActionName::StackSwap => Action::StackSwap,
        ActionName::StartDrag => Action::StartDrag,
        ActionName::Stop => Action::Stop,
        ActionName::StopSounds => Action::StopSounds,
        ActionName::StoreRegister => store_register.parse_next(i)?,
        ActionName::StrictEquals => Action::StrictEquals,
        ActionName::StringAdd => Action::StringAdd,
        ActionName::StringEquals => Action::StringEquals,
        ActionName::StringExtract => Action::StringExtract,
        ActionName::StringGreater => Action::StringGreater,
        ActionName::StringLength => Action::StringLength,
        ActionName::StringLess => Action::StringLess,
        ActionName::Subtract => Action::Subtract,
        ActionName::TargetPath => Action::TargetPath,
        ActionName::Throw => Action::Throw,
        ActionName::ToInteger => Action::ToInteger,
        ActionName::ToNumber => Action::ToNumber,
        ActionName::ToString => Action::ToString,
        ActionName::ToggleQuality => Action::ToggleQuality,
        ActionName::Trace => Action::Trace,
        ActionName::Try => try_catch.parse_next(i)?,
        ActionName::TypeOf => Action::TypeOf,
        ActionName::WaitForFrame => wait_for_frame.parse_next(i)?,
        ActionName::WaitForFrame2 => wait_for_frame_2.parse_next(i)?,
        ActionName::With => with.parse_next(i)?,
    })
}

pub fn get_url(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let url = string.parse_next(i)?;
    TokenKind::Comma.parse_next(i)?;
    let target = string.parse_next(i)?;
    Ok(Action::GetUrl { url, target })
}

pub fn get_url_2(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let load_variables = bool.parse_next(i)?;
    TokenKind::Comma.parse_next(i)?;
    let load_target = bool.parse_next(i)?;
    TokenKind::Comma.parse_next(i)?;
    let method = u8.parse_next(i)?;
    Ok(Action::GetUrl2 {
        load_variables,
        load_target,
        method,
    })
}

pub fn if_(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let label = label.parse_next(i)?;
    Ok(Action::If(label))
}

pub fn jump(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let label = label.parse_next(i)?;
    Ok(Action::Jump(label))
}

pub fn goto_frame(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let frame = u16.parse_next(i)?;
    Ok(Action::GotoFrame(frame))
}

pub fn goto_frame_2(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let use_scene_bias = bool.parse_next(i)?;
    TokenKind::Comma.parse_next(i)?;
    let play = bool.parse_next(i)?;
    let scene_bias = if use_scene_bias {
        TokenKind::Comma.parse_next(i)?;
        u16.parse_next(i)?
    } else {
        0
    };
    Ok(Action::GotoFrame2 { scene_bias, play })
}

pub fn goto_label(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let label = string.parse_next(i)?;
    Ok(Action::GotoLabel(label))
}

pub fn store_register(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let n = u8.parse_next(i)?;
    Ok(Action::StoreRegister(n))
}

pub fn register(i: &mut Tokens<'_>) -> ModalResult<u8> {
    let token = any.parse_next(i)?;
    match token.kind {
        TokenKind::Register(n) => Ok(n),
        _ => fail.parse_next(i),
    }
}

pub fn set_target(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let value = string.parse_next(i)?;
    Ok(Action::SetTarget(value))
}

pub fn push(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let values = separated(0.., push_value, TokenKind::Comma).parse_next(i)?;
    Ok(Action::Push(values))
}

pub fn constant_pool(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let values = separated(0.., string, TokenKind::Comma).parse_next(i)?;
    Ok(Action::ConstantPool(values))
}

pub fn wait_for_frame(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let frame = u16.parse_next(i)?;
    TokenKind::Comma.parse_next(i)?;
    let skip_count = u8.parse_next(i)?;
    Ok(Action::WaitForFrame { frame, skip_count })
}

pub fn wait_for_frame_2(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let skip_count = u8.parse_next(i)?;
    Ok(Action::WaitForFrame2 { skip_count })
}

pub fn define_function(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let name = string.parse_next(i)?;
    TokenKind::Comma.parse_next(i)?;
    let num_args = integer.parse_next(i)?;
    let args = if num_args > 0 {
        TokenKind::Comma.parse_next(i)?;
        separated(num_args as usize, string, TokenKind::Comma).parse_next(i)?
    } else {
        vec![]
    };
    TokenKind::OpenBrace.parse_next(i)?;
    let body = actions(true).parse_next(i)?;
    TokenKind::CloseBrace.parse_next(i)?;
    Ok(Action::DefineFunction {
        name,
        params: args,
        actions: body,
    })
}

pub fn with(i: &mut Tokens<'_>) -> ModalResult<Action> {
    TokenKind::OpenBrace.parse_next(i)?;
    let body = actions(true).parse_next(i)?;
    TokenKind::CloseBrace.parse_next(i)?;
    Ok(Action::With(body))
}

pub fn try_catch(i: &mut Tokens<'_>) -> ModalResult<Action> {
    let catch_target = opt(alt((
        string.map(CatchTarget::Variable),
        register.map(CatchTarget::Register),
    )))
    .parse_next(i)?;
    TokenKind::OpenBrace.parse_next(i)?;
    let try_body = actions(true).parse_next(i)?;
    TokenKind::CloseBrace.parse_next(i)?;

    let catch_body = if opt((take_while(1.., TokenKind::Newline), TokenKind::Catch))
        .parse_next(i)?
        .is_some()
    {
        TokenKind::OpenBrace.parse_next(i)?;
        let catch_body = actions(true).parse_next(i)?;
        TokenKind::CloseBrace.parse_next(i)?;
        Some((
            catch_target.unwrap_or_else(|| CatchTarget::Variable("".to_string())),
            catch_body,
        ))
    } else {
        None
    };

    let finally_body = if opt((take_while(1.., TokenKind::Newline), TokenKind::Finally))
        .parse_next(i)?
        .is_some()
    {
        TokenKind::OpenBrace.parse_next(i)?;
        let finally_body = actions(true).parse_next(i)?;
        TokenKind::CloseBrace.parse_next(i)?;
        Some(finally_body)
    } else {
        None
    };

    Ok(Action::Try {
        try_body,
        catch_body,
        finally_body,
    })
}

pub fn string(i: &mut Tokens<'_>) -> ModalResult<String> {
    // TODO parse
    TokenKind::String.parse_next(i).map(|t| t.raw.to_owned())
}

pub fn bool(i: &mut Tokens<'_>) -> ModalResult<bool> {
    alt((
        TokenKind::True.map(|_| true),
        TokenKind::False.map(|_| false),
    ))
    .parse_next(i)
}

pub fn label(i: &mut Tokens<'_>) -> ModalResult<String> {
    TokenKind::Identifier
        .context(StrContext::Label("label"))
        .parse_next(i)
        .map(|t| t.raw.to_owned())
}

fn integer_or_float(i: &mut Tokens<'_>) -> ModalResult<PushValue> {
    let raw = alt((TokenKind::Integer, TokenKind::Float))
        .parse_next(i)?
        .raw;
    if let Ok(n) = raw.parse::<i32>() {
        return Ok(PushValue::Integer(n));
    }
    raw.parse::<f64>()
        .map_err(|_| ParserError::from_input(&raw))
        .map(PushValue::Float)
}

fn u8(i: &mut Tokens<'_>) -> ModalResult<u8> {
    let raw = TokenKind::Integer.parse_next(i)?.raw;
    raw.parse::<u8>().map_err(|_| ParserError::from_input(&raw))
}

fn u16(i: &mut Tokens<'_>) -> ModalResult<u16> {
    let raw = TokenKind::Integer.parse_next(i)?.raw;
    raw.parse::<u16>()
        .map_err(|_| ParserError::from_input(&raw))
}

fn integer(i: &mut Tokens<'_>) -> ModalResult<i32> {
    let raw = TokenKind::Integer.parse_next(i)?.raw;
    raw.parse::<i32>()
        .map_err(|_| ParserError::from_input(&raw))
}

fn float(i: &mut Tokens<'_>) -> ModalResult<f64> {
    let raw = TokenKind::Float.parse_next(i)?.raw;
    raw.parse::<f64>()
        .map_err(|_| ParserError::from_input(&raw))
}

pub(crate) fn push_value(i: &mut Tokens<'_>) -> ModalResult<PushValue> {
    let checkpoint = i.checkpoint();
    let token = any.parse_next(i)?;
    match token.kind {
        TokenKind::String => {
            i.reset(&checkpoint);
            string.parse_next(i).map(PushValue::String)
        }
        TokenKind::Register(n) => Ok(PushValue::Register(n)),
        TokenKind::Constant(n) => Ok(PushValue::Constant(n)),
        TokenKind::False => Ok(PushValue::False),
        TokenKind::True => Ok(PushValue::True),
        TokenKind::Undefined => Ok(PushValue::Undefined),
        TokenKind::Null => Ok(PushValue::Null),
        TokenKind::Integer => {
            i.reset(&checkpoint);
            integer_or_float.parse_next(i)
        }
        TokenKind::Float => {
            i.reset(&checkpoint);
            float.parse_next(i).map(PushValue::Float)
        }
        _ => fail
            .context(StrContext::Label("value"))
            .context(StrContext::Expected(StrContextValue::Description("string")))
            .context(StrContext::Expected(StrContextValue::Description(
                "constant",
            )))
            .context(StrContext::Expected(StrContextValue::Description(
                "register",
            )))
            .context(StrContext::Expected(StrContextValue::Description(
                "integer",
            )))
            .context(StrContext::Expected(StrContextValue::Description("float")))
            .context(StrContext::Expected(StrContextValue::StringLiteral("true")))
            .context(StrContext::Expected(StrContextValue::StringLiteral(
                "false",
            )))
            .context(StrContext::Expected(StrContextValue::StringLiteral("null")))
            .context(StrContext::Expected(StrContextValue::StringLiteral(
                "undefined",
            )))
            .parse_next(i),
    }
}

#[cfg(test)]
mod tests {
    use crate::internal::as2_pcode::lexer::tokens::{ActionName, TokenKind};
    use crate::internal::as2_pcode::parser::parse_actions;
    use crate::internal::as2_pcode::parser::tests::build_tokens;
    use crate::internal::as2_pcode::pcode::{Action, Actions, CatchTarget, PushValue};
    use indexmap::IndexMap;

    macro_rules! trivial_action_tests {
        ($($test_name:ident => $action_name:ident),* $(,)?) => {
            $(
                #[test]
                fn $test_name() {
                    let tokens = [(TokenKind::ActionName(ActionName::$action_name), "")];
                    let tokens = build_tokens(&tokens);
                    assert_eq!(
                        parse_actions(&tokens),
                        Ok(Actions {
                            actions: vec![Action::$action_name],
                            label_positions: Default::default()
                        })
                    )
                }
            )*
        }
    }

    trivial_action_tests! {
        test_add => Add,
        test_add2 => Add2,
        test_and => And,
        test_asciitochar => AsciiToChar,
        test_bitand => BitAnd,
        test_bitlshift => BitLShift,
        test_bitor => BitOr,
        test_bitrshift => BitRShift,
        test_biturshift => BitURShift,
        test_bitxor => BitXor,
        test_call => Call,
        test_call_function => CallFunction,
        test_call_method => CallMethod,
        test_cast_op => CastOp,
        test_char_to_ascii => CharToAscii,
        test_clone_sprite => CloneSprite,
        test_decrement => Decrement,
        test_define_local => DefineLocal,
        test_definelocal2 => DefineLocal2,
        test_delete => Delete,
        test_delete2 => Delete2,
        test_divide => Divide,
        test_end => End,
        test_end_drag => EndDrag,
        test_enumerate => Enumerate,
        test_enumerate2 => Enumerate2,
        test_equals => Equals,
        test_equals2 => Equals2,
        test_extends => Extends,
        test_get_member => GetMember,
        test_get_property => GetProperty,
        test_get_timer => GetTime,
        test_get_variable => GetVariable,
        test_greater => Greater,
        test_implements_op => ImplementsOp,
        test_increment => Increment,
        test_init_array => InitArray,
        test_init_object => InitObject,
        test_instanceof => InstanceOf,
        test_less => Less,
        test_less2 => Less2,
        test_mbasciitochar => MBAsciiToChar,
        test_mbchartoascii => MBCharToAscii,
        test_mbstringextract => MBStringExtract,
        test_mbstringlength => MBStringLength,
        test_modulo => Modulo,
        test_multiply => Multiply,
        test_new_method => NewMethod,
        test_new_object => NewObject,
        test_next_frame => NextFrame,
        test_not => Not,
        test_or => Or,
        test_pop => Pop,
        test_pplay => Play,
        test_pushduplicate => PushDuplicate,
        test_random_number => RandomNumber,
        test_remove_sprite => RemoveSprite,
        test_return => Return,
        test_set_member => SetMember,
        test_set_property => SetProperty,
        test_set_target2 => SetTarget2,
        test_set_variable => SetVariable,
        test_sack_swap => StackSwap,
        test_start_drag => StartDrag,
        test_stop => Stop,
        test_stop_sounds => StopSounds,
        test_subtract => Subtract,
        test_target_path => TargetPath,
        test_throw => Throw,
        test_tointeger => ToInteger,
        test_tonumber => ToNumber,
        test_tostring => ToString,
        test_toggle_quality => ToggleQuality,
        test_strictequals => StrictEquals,
        test_stringextract => StringExtract,
        test_stringadd => StringAdd,
        test_stringequals => StringEquals,
        test_stringgreater => StringGreater,
        test_stringlength => StringLength,
        test_stringless => StringLess,
        test_trace => Trace,
        test_typeof => TypeOf,
    }

    #[test]
    fn test_multiple_labels() {
        let tokens = build_tokens(&[
            (TokenKind::Identifier, "start"),
            (TokenKind::Colon, ":"),
            (TokenKind::Identifier, "end"),
            (TokenKind::Colon, ":"),
            (TokenKind::ActionName(ActionName::Trace), "trace"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::Trace],
                label_positions: IndexMap::from([("start".to_owned(), 0), ("end".to_owned(), 0)])
            })
        )
    }

    #[test]
    fn test_constant_pool() {
        let tokens = build_tokens(&[
            (
                TokenKind::ActionName(ActionName::ConstantPool),
                "constantPool",
            ),
            (TokenKind::String, "foo"),
            (TokenKind::Comma, ","),
            (TokenKind::String, "bar"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::ConstantPool(vec![
                    "foo".to_owned(),
                    "bar".to_owned()
                ])],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_push_all_values() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::Push), "push"),
            (TokenKind::Register(0), "0"),
            (TokenKind::Comma, ","),
            (TokenKind::String, "foo"),
            (TokenKind::Comma, ","),
            (TokenKind::Integer, "1"),
            (TokenKind::Comma, ","),
            (TokenKind::Float, "1.0"),
            (TokenKind::Comma, ","),
            (TokenKind::True, "true"),
            (TokenKind::Comma, ","),
            (TokenKind::False, "false"),
            (TokenKind::Comma, ","),
            (TokenKind::Null, "null"),
            (TokenKind::Comma, ","),
            (TokenKind::Undefined, "undefined"),
            (TokenKind::Comma, ","),
            (TokenKind::Constant(1), "1"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::Push(vec![
                    PushValue::Register(0),
                    PushValue::String("foo".to_owned()),
                    PushValue::Integer(1),
                    PushValue::Float(1.0),
                    PushValue::True,
                    PushValue::False,
                    PushValue::Null,
                    PushValue::Undefined,
                    PushValue::Constant(1),
                ])],
                label_positions: Default::default(),
            })
        )
    }

    #[test]
    fn test_if() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::If), "if"),
            (TokenKind::Identifier, "end"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::If("end".to_owned())],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_jump() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::Jump), "jump"),
            (TokenKind::Identifier, "end"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::Jump("end".to_owned())],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_store_register() {
        let tokens = build_tokens(&[
            (
                TokenKind::ActionName(ActionName::StoreRegister),
                "StoreRegister",
            ),
            (TokenKind::Integer, "0"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::StoreRegister(0)],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_define_function() {
        let tokens = build_tokens(&[
            (
                TokenKind::ActionName(ActionName::DefineFunction),
                "DefineFunction",
            ),
            (TokenKind::String, "foo"),
            (TokenKind::Comma, ","),
            (TokenKind::Integer, "1"),
            (TokenKind::Comma, ","),
            (TokenKind::String, "a"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::ActionName(ActionName::Push), "push"),
            (TokenKind::String, "a"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Trace), "trace"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::DefineFunction {
                    name: "foo".to_string(),
                    params: vec!["a".to_string()],
                    actions: Actions {
                        actions: vec![
                            Action::Push(vec![PushValue::String("a".to_owned())]),
                            Action::Trace
                        ],
                        label_positions: Default::default()
                    },
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_get_url() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::GetUrl), "getUrl"),
            (TokenKind::String, "foo"),
            (TokenKind::Comma, ","),
            (TokenKind::String, "bar"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::GetUrl {
                    url: "foo".to_owned(),
                    target: "bar".to_owned()
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_get_url_2() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::GetUrl2), "getUrl2"),
            (TokenKind::True, "true"),
            (TokenKind::Comma, ","),
            (TokenKind::False, "false"),
            (TokenKind::Comma, ","),
            (TokenKind::Integer, "1"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::GetUrl2 {
                    load_variables: true,
                    load_target: false,
                    method: 1
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_goto_frame() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::GotoFrame), "gotoFrame"),
            (TokenKind::Integer, "123"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::GotoFrame(123)],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_goto_frame_2_with_scene_bias() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::GotoFrame2), "gotoFrame2"),
            (TokenKind::True, "true"),
            (TokenKind::Comma, ","),
            (TokenKind::True, "true"),
            (TokenKind::Comma, ","),
            (TokenKind::Integer, "123"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::GotoFrame2 {
                    scene_bias: 123,
                    play: true
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_goto_frame_2_without_scene_bias() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::GotoFrame2), "gotoFrame2"),
            (TokenKind::False, "false"),
            (TokenKind::Comma, ","),
            (TokenKind::True, "true"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::GotoFrame2 {
                    scene_bias: 0,
                    play: true
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_goto_label() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::GotoLabel), "gotoLabel"),
            (TokenKind::String, "foo"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::GotoLabel("foo".to_owned())],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_try_catch_finally() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::Try), "Try"),
            (TokenKind::Register(0), "register0"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Play), "play"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
            (TokenKind::Newline, "\n"),
            (TokenKind::Catch, "Catch"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Stop), "stop"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
            (TokenKind::Newline, "\n"),
            (TokenKind::Finally, "Finally"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Return), "return"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
            (TokenKind::Newline, "\n"),
        ]);

        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::Try {
                    try_body: Actions {
                        actions: vec![Action::Play],
                        label_positions: Default::default(),
                    },
                    catch_body: Some((
                        CatchTarget::Register(0),
                        Actions {
                            actions: vec![Action::Stop],
                            label_positions: Default::default()
                        }
                    )),
                    finally_body: Some(Actions {
                        actions: vec![Action::Return],
                        label_positions: Default::default()
                    })
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_try_catch() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::Try), "Try"),
            (TokenKind::String, "e"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Play), "play"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
            (TokenKind::Newline, "\n"),
            (TokenKind::Catch, "Catch"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Stop), "stop"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
        ]);

        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::Try {
                    try_body: Actions {
                        actions: vec![Action::Play],
                        label_positions: Default::default(),
                    },
                    catch_body: Some((
                        CatchTarget::Variable("e".to_owned()),
                        Actions {
                            actions: vec![Action::Stop],
                            label_positions: Default::default()
                        }
                    )),
                    finally_body: None
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_try_finally() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::Try), "Try"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Play), "play"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
            (TokenKind::Newline, "\n"),
            (TokenKind::Finally, "Finally"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::Stop), "stop"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
        ]);

        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::Try {
                    try_body: Actions {
                        actions: vec![Action::Play],
                        label_positions: Default::default(),
                    },
                    catch_body: None,
                    finally_body: Some(Actions {
                        actions: vec![Action::Stop],
                        label_positions: Default::default()
                    })
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_wait_for_frame() {
        let tokens = build_tokens(&[
            (
                TokenKind::ActionName(ActionName::WaitForFrame),
                "waitForFrame",
            ),
            (TokenKind::Integer, "123"),
            (TokenKind::Comma, ","),
            (TokenKind::Integer, "45"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::WaitForFrame {
                    frame: 123,
                    skip_count: 45
                }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_wait_for_frame_2() {
        let tokens = build_tokens(&[
            (
                TokenKind::ActionName(ActionName::WaitForFrame2),
                "waitForFrame2",
            ),
            (TokenKind::Integer, "123"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::WaitForFrame2 { skip_count: 123 }],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_set_target() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::SetTarget), "setTarget"),
            (TokenKind::String, "foo"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::SetTarget("foo".to_owned())],
                label_positions: Default::default()
            })
        )
    }

    #[test]
    fn test_label_on_own_line() {
        let tokens = build_tokens(&[
            (TokenKind::Identifier, "foo"),
            (TokenKind::Colon, ":"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::End), "end"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::End],
                label_positions: IndexMap::from([("foo".to_owned(), 0)])
            })
        )
    }

    #[test]
    fn test_with() {
        let tokens = build_tokens(&[
            (TokenKind::ActionName(ActionName::With), "with"),
            (TokenKind::OpenBrace, "{"),
            (TokenKind::Newline, "\n"),
            (TokenKind::ActionName(ActionName::End), "end"),
            (TokenKind::Newline, "\n"),
            (TokenKind::CloseBrace, "}"),
        ]);
        assert_eq!(
            parse_actions(&tokens),
            Ok(Actions {
                actions: vec![Action::With(Actions {
                    actions: vec![Action::End],
                    label_positions: Default::default()
                })],
                label_positions: Default::default()
            })
        )
    }
}
