// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

//! Comprehensive test suite for the laburnum_syntax procedural macro
//!
//! This test suite covers:
//! - AST (Abstract Syntax Tree) generation
//! - CST (Concrete Syntax Tree) generation
//! - Error handling and reporting
//! - Edge cases and invalid inputs
//! - Integration with real-world use cases

mod ast_tests;
mod cst_tests;
mod error_tests;
mod test_macros;

use {
  crate::{
    Args,
    process,
  },
  ferrotype::Ferrotype,
  quote::quote,
};

// Integration tests that test the full macro behavior

#[test_log::test]
fn test_macro_attribute_parsing() {
  // Test that the macro correctly parses various attribute combinations

  // Test AST
  let args = Args::parse(quote! { AST });
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(!args.is_error_node());
  assert!(!args.has_semantic_tokens());

  // Test CST
  let args = Args::parse(quote! { CST });
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(!args.is_error_node());
  assert!(!args.has_semantic_tokens());

  // Test AST,error
  let args = Args::parse(quote! { AST, error });
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(args.is_error_node());
  assert!(!args.has_semantic_tokens());

  // Test CST,error
  let args = Args::parse(quote! { CST, error });
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(args.is_error_node());
  assert!(!args.has_semantic_tokens());

  // Test AST,allow_semantic
  let args = Args::parse(quote! { AST, allow_semantic });
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(!args.is_error_node());
  assert!(args.has_semantic_tokens());

  // Test CST,allow_semantic
  let args = Args::parse(quote! { CST, allow_semantic });
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(!args.is_error_node());
  assert!(args.has_semantic_tokens());

  // Test AST,error,allow_semantic
  let args = Args::parse(quote! { AST, error, allow_semantic });
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(args.is_error_node());
  assert!(args.has_semantic_tokens());

  // Test CST,error,allow_semantic
  let args = Args::parse(quote! { CST, error, allow_semantic });
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(args.is_error_node());
  assert!(args.has_semantic_tokens());
}

#[test_log::test]
fn test_real_world_ast_example() {
  let mut snapshot = Ferrotype::new();

  // This is a real example from the codebase
  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct Function {
      visibility: Field<Enum<crate::modifier::Visibility>>,
      ident: NodeId<crate::symbol::Ident>,
      generic_parameters: Option<NodeId<crate::ty::GenericParameters>>,
      parameters: Vec<crate::ty::Parameters>,
      return_ty: Option<NodeId<crate::ty::Path>>,
      effects_required: Vec<NodeId<crate::ty::Path>>,
      effects_handled: Vec<NodeId<crate::ty::Path>>,
      body: NodeId<crate::expression::Block>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(AST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);

      // Verify the output contains expected elements
      let output_str = output.to_string();
      assert!(output_str.contains("impl Function"));
      assert!(output_str.contains("get_visibility"));
      assert!(output_str.contains("get_ident"));
      assert!(output_str.contains("get_body"));
    },
    | Err(e) => {
      panic!("Real-world example failed: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_reserved_keyword_field_name() {
  // This test ensures we properly reject field names that would convert to
  // reserved keywords
  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct BadFieldName {
      self_: NodeId<crate::Type>,
    }
  };

  // This should return an error during processing
  match process(quote!(AST), input) {
    | Ok(_) => {
      panic!("Expected ReservedKeywordFieldName error");
    },
    | Err(e) => {
      let error_message = e.to_string();
      assert!(
        error_message.contains("self_") && error_message.contains("Self"),
        "Error message should mention 'self_' and 'Self', got: {error_message}",
      );
    },
  }
}

#[test_log::test]
fn test_real_world_cst_example() {
  let mut snapshot = Ferrotype::new();

  // Example of what a CST node might look like
  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct FunctionDeclaration {
      span: Span,
      pub_token: Option<NodeId<crate::Token>>,
      fn_token: NodeId<crate::Token>,
      identifier: NodeId<crate::Identifier>,
      generic_params: Option<NodeId<crate::GenericParameters>>,
      lparen: NodeId<crate::Token>,
      params: Vec<NodeId<crate::Parameter>>,
      rparen: NodeId<crate::Token>,
      return_type: Option<NodeId<crate::ReturnType>>,
      body: NodeId<crate::BlockExpression>,
      trivia: Vec<crate::Trivia>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);

      let output_str = output.to_string();
      assert!(output_str.contains("impl FunctionDeclaration"));
    },
    | Err(e) => {
      panic!("CST example failed: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_complex_enum_field() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct ComplexEnum {
      // Real example from the codebase
      visibility: Field<Enum<crate::modifier::Visibility>>,
      // Multiple enum variants
      expr: EnumNodeId<crate::expression::Inline>,
      // Optional enum
      maybe_expr: Option<EnumNodeId<crate::expression::Expression>>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(AST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Complex enum test failed: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_edge_case_many_union_types() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct ManyUnionTypes {
      // Test with many type parameters
      two_types: NodeId<crate::Type1, crate::Type2>,
      three_types: NodeId<crate::Type1, crate::Type2, crate::Type3>,
      four_types: NodeId<crate::Type1, crate::Type2, crate::Type3, crate::Type4>,
      vec_multi: Vec<crate::declaration::Function, crate::declaration::Import>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(AST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Many union types test failed: {e:?}");
    },
  }

  snapshot.print_stdout();

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_deeply_nested_types() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct DeeplyNested {
      // Nested options and vecs
      nested1: Option<Vec<NodeId<crate::Type>>>,
      nested2: Vec<Option<NodeId<crate::Type>>>,
      nested3: Option<Vec<crate::declaration::Function, crate::declaration::Import>>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(AST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Deeply nested types test failed: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_attributes_preserved() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[derive(Debug, Clone, PartialEq)]
    #[cfg(feature = "serde")]
    #[laburnum_syntax(AST)]
    pub struct PreserveAttributes {
      #[serde(rename = "customName")]
      field: NodeId<crate::Type>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(AST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);

      // Check that derive and other attributes are preserved
      let output_str = output.to_string();
      assert!(output_str.contains("derive"));
      assert!(output_str.contains("Debug"));
      assert!(output_str.contains("Clone"));
    },
    | Err(e) => {
      panic!("Preserve attributes test failed: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

// Comprehensive tests for Args parsing with span tracking
#[test_log::test]
fn test_args_parsing_with_spans() {
  // Test CST with span tracking
  let cst_tokens = quote! { CST };
  let args = Args::parse(cst_tokens.clone());
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(args.cst_span.is_some());
  assert!(args.ast_span.is_none());

  // Test AST with span tracking
  let ast_tokens = quote! { AST };
  let args = Args::parse(ast_tokens.clone());
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(args.ast_span.is_some());
  assert!(args.cst_span.is_none());

  // Test multiple flags
  let multi_tokens = quote! { CST, error, allow_semantic };
  let args = Args::parse(multi_tokens);
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(args.is_error_node());
  assert!(args.has_semantic_tokens());
  assert!(args.cst_span.is_some());
  assert!(args.ast_span.is_none());
}

#[test_log::test]
fn test_args_parsing_edge_cases() {
  // Test empty args
  let empty_tokens = quote! {};
  let args = Args::parse(empty_tokens);
  assert!(!args.is_ast());
  assert!(!args.is_cst());
  assert!(!args.is_error_node());
  assert!(!args.has_semantic_tokens());
  assert!(args.cst_span.is_none());
  assert!(args.ast_span.is_none());

  // Test with extra commas
  let extra_comma_tokens = quote! { AST, };
  let args = Args::parse(extra_comma_tokens);
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(args.ast_span.is_some());

  // Test with whitespace variations
  let whitespace_tokens = quote! { CST , error };
  let args = Args::parse(whitespace_tokens);
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(args.is_error_node());
  assert!(args.cst_span.is_some());

  // Test unknown flags are ignored
  let unknown_tokens = quote! { AST, unknown_flag, error };
  let args = Args::parse(unknown_tokens);
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(args.is_error_node());
  assert!(!args.has_semantic_tokens());
}

#[test_log::test]
fn test_args_parsing_complex_semantic_patterns() {
  // Note: Current implementation doesn't parse complex semantic token types,
  // but we test that the basic flags still work in their presence

  // This simulates what would happen with: CST, SemanticTokenType::String
  let complex_tokens = quote! { CST, SemanticTokenType };
  let args = Args::parse(complex_tokens);
  assert!(!args.is_ast());
  assert!(args.is_cst());
  assert!(!args.is_error_node());
  assert!(!args.has_semantic_tokens());
  assert!(args.cst_span.is_some());

  // This tests that we can handle path-like tokens gracefully
  let path_tokens = quote! { AST, SomeModule::SomeType };
  let args = Args::parse(path_tokens);
  assert!(args.is_ast());
  assert!(!args.is_cst());
  assert!(args.ast_span.is_some());
}

#[test_log::test]
fn test_args_span_accuracy() {
  // Test that spans point to the correct tokens
  let tokens = quote! { AST, error };
  let args = Args::parse(tokens.clone());

  assert!(args.is_ast());
  assert!(args.is_error_node());
  assert!(args.ast_span.is_some());

  // Verify the span is not call_site (which would indicate we're not tracking
  // properly)
  let ast_span = args.ast_span.unwrap();

  // In a real scenario, these would be different, but in tests they might be
  // the same The important thing is that we're capturing the span from the
  // actual token
  // Note: proc_macro2::Span doesn't expose start/end methods, but we can verify
  // it exists
  assert!(!format!("{ast_span:?}").is_empty());
}

#[test_log::test]
fn test_args_parsing_all_combinations() {
  // Test all valid flag combinations
  let test_cases = vec![
    (quote! { AST }, true, false, false, false),
    (quote! { CST }, false, true, false, false),
    (quote! { AST, error }, true, false, true, false),
    (quote! { CST, error }, false, true, true, false),
    (quote! { AST, allow_semantic }, true, false, false, true),
    (quote! { CST, allow_semantic }, false, true, false, true),
    (
      quote! { AST, error, allow_semantic },
      true,
      false,
      true,
      true,
    ),
    (
      quote! { CST, error, allow_semantic },
      false,
      true,
      true,
      true,
    ),
  ];

  for (tokens, expect_ast, expect_cst, expect_error, expect_semantic) in
    test_cases
  {
    let args = Args::parse(tokens);
    assert_eq!(args.is_ast(), expect_ast, "AST flag mismatch");
    assert_eq!(args.is_cst(), expect_cst, "CST flag mismatch");
    assert_eq!(args.is_error_node(), expect_error, "error flag mismatch");
    assert_eq!(
      args.has_semantic_tokens(),
      expect_semantic,
      "semantic flag mismatch"
    );

    // Check span tracking
    if expect_ast {
      assert!(args.ast_span.is_some(), "AST span should be tracked");
    } else {
      assert!(args.ast_span.is_none(), "AST span should not be tracked");
    }

    if expect_cst {
      assert!(args.cst_span.is_some(), "CST span should be tracked");
    } else {
      assert!(args.cst_span.is_none(), "CST span should not be tracked");
    }
  }
}

#[test_log::test]
fn test_process_function_uses_correct_spans() {
  // Test that the process function will use the tracked spans instead of
  // call_site This is testing the integration between Args parsing and the
  // process function

  // Test conflicting flags - both AST and CST
  let conflicting_tokens = quote! { AST, CST };
  let args = Args::parse(conflicting_tokens.clone());

  // Verify that both spans are captured
  assert!(args.ast_span.is_some());
  assert!(args.cst_span.is_some());
  assert!(args.is_ast());
  assert!(args.is_cst());

  // The process function should use these spans for error reporting
  let dummy_item = quote! {
    pub struct TestStruct {
      field: NodeId<crate::Type>,
    }
  };

  // This should fail with both flags set
  match process(conflicting_tokens, dummy_item) {
    | Ok(_) => panic!("Expected error for conflicting flags"),
    | Err(e) => {
      // The error should be generated, and when we fix the spans,
      // it should point to the actual tokens instead of call_site
      assert!(e.to_string().contains("Cannot specify both AST and CST"));
    },
  }
}

// Test to ensure macro hygiene
#[test_log::test]
fn test_macro_hygiene() {
  let mut snapshot = Ferrotype::new();

  // Use names that might conflict with generated code
  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct HygieneTest {
      my_self: NodeId<crate::Type>,
      ast: NodeId<crate::AST>,
      node: NodeId<crate::Node>,
      field: Field<bool>,
      // Use names that are close to keywords but won't convert to them
      my_type: NodeId<crate::Type>,
      my_impl: Option<NodeId<crate::Type>>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(AST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);

      // Should compile without hygiene issues
      let output_str = output.to_string();
      assert!(output_str.contains("impl HygieneTest"));
    },
    | Err(e) => {
      panic!("Macro hygiene test failed: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}
