package jadx.tests.api;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.JadxInternalAccess;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaVariable;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.ResourcesLoader;
import jadx.api.args.GeneratedRenamesMappingFileMode;
import jadx.api.data.IJavaNodeRef;
import jadx.api.data.impl.JadxCodeData;
import jadx.api.data.impl.JadxCodeRename;
import jadx.api.data.impl.JadxNodeRef;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.plugins.CustomResourcesLoader;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.BinaryXMLStrings;
import jadx.core.xmlgen.IResTableParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.tests.api.compiler.CompilerOptions;
import jadx.tests.api.compiler.JavaUtils;
import jadx.tests.api.compiler.TestCompiler;
import jadx.tests.api.utils.TestFilesGetter;
import jadx.tests.api.utils.TestUtils;

import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.apache.commons.lang3.StringUtils.rightPad;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

public abstract class IntegrationTest extends TestUtils {
	private static final Logger LOG = LoggerFactory.getLogger(IntegrationTest.class);

	private static final String TEST_DIRECTORY = "src/test/java";
	private static final String TEST_DIRECTORY2 = "jadx-core/" + TEST_DIRECTORY;

	private static final String DEFAULT_INPUT_PLUGIN = "dx";
	/**
	 * Set 'TEST_INPUT_PLUGIN' env variable to use 'java' or 'dx' input in tests
	 */
	static final boolean USE_JAVA_INPUT = Utils.getOrElse(System.getenv("TEST_INPUT_PLUGIN"), DEFAULT_INPUT_PLUGIN).equals("java");

	/**
	 * Run auto check method if defined:
	 *
	 * <pre>
	 * public void check() {
	 * }
	 * </pre>
	 */
	private static final String CHECK_METHOD_NAME = "check";

	protected JadxArgs args;

	protected boolean compile;
	private CompilerOptions compilerOptions;

	private boolean saveTestJar = false;

	protected Map<Integer, String> resMap = Collections.emptyMap();

	private boolean allowWarnInCode;
	private boolean printLineNumbers;
	private boolean printOffsets;
	private boolean printDisassemble;
	private @Nullable Boolean useJavaInput;
	private boolean removeParentClassOnInput;

	private @Nullable TestCompiler sourceCompiler;
	private @Nullable TestCompiler decompiledCompiler;

	/**
	 * Run check method on decompiled code even if source check method not found.
	 * Useful for Smali tests if check method added into Smali code
	 */
	private boolean forceDecompiledCheck = false;

	protected JadxDecompiler jadxDecompiler;

	@TempDir
	Path testDir;

	@BeforeEach
	public void init() {
		this.compile = true;
		this.compilerOptions = new CompilerOptions();
		this.resMap = Collections.emptyMap();
		this.removeParentClassOnInput = true;
		this.useJavaInput = null;

		args = new JadxArgs();
		args.setOutDir(testDir.toFile());
		args.setShowInconsistentCode(true);
		args.setThreadsCount(1);
		args.setSkipResources(true);
		args.setCommentsLevel(CommentsLevel.DEBUG);
		args.setDeobfuscationOn(false);
		args.setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode.IGNORE);
		args.setRunDebugChecks(true);
		args.setFilesGetter(new TestFilesGetter(testDir));

		// use the same values on all systems
		args.setFsCaseSensitive(false);
		args.setCodeNewLineStr("\n");
		args.setCodeIndentStr(JadxArgs.DEFAULT_INDENT_STR);
	}

	@AfterEach
	public void after() throws IOException {
		close(jadxDecompiler);
		close(sourceCompiler);
		close(decompiledCompiler);
	}

	private void close(Closeable closeable) throws IOException {
		if (closeable != null) {
			closeable.close();
		}
	}

	public void setOutDirSuffix(String suffix) {
		args.setOutDir(new File(testDir.toFile(), suffix));
	}

	public String getTestName() {
		return this.getClass().getSimpleName();
	}

	public String getTestPkg() {
		return this.getClass().getPackage().getName().replace("jadx.tests.integration.", "");
	}

	public ClassNode getClassNode(Class<?> clazz) {
		try {
			List<File> files = compileClass(clazz);
			assertThat(files).as("File list is empty").isNotEmpty();
			return getClassNodeFromFiles(files, clazz.getName());
		} catch (Exception e) {
			LOG.error("Failed to get class node", e);
			fail(e.getMessage());
			return null;
		}
	}

	public List<ClassNode> getClassNodes(Class<?>... classes) {
		try {
			assertThat(classes).as("Class list is empty").isNotEmpty();
			List<File> srcFiles = Stream.of(classes).map(this::getSourceFileForClass).collect(Collectors.toList());
			List<File> clsFiles = compileSourceFiles(srcFiles);
			assertThat(clsFiles).as("Class files list is empty").isNotEmpty();
			return decompileFiles(clsFiles);
		} catch (Exception e) {
			LOG.error("Failed to get class node", e);
			fail(e.getMessage());
			return null;
		}
	}

	public ClassNode getClassNodeFromFiles(List<File> files, String clsName) {
		jadxDecompiler = loadFiles(files);
		RootNode root = JadxInternalAccess.getRoot(jadxDecompiler);

		ClassNode cls = root.resolveClass(clsName);
		assertThat(cls).as("Class not found: " + clsName).isNotNull();
		if (removeParentClassOnInput) {
			assertThat(clsName).isEqualTo(cls.getClassInfo().getFullName());
		} else {
			LOG.info("Convert back to top level: {}", cls);
			cls.getTopParentClass().decompile(); // keep correct process order
			cls.notInner();
		}
		decompileAndCheck(cls);
		return cls;
	}

	public List<ClassNode> decompileFiles(List<File> files) {
		jadxDecompiler = loadFiles(files);
		List<ClassNode> sortedClsNodes = jadxDecompiler.getDecompileScheduler()
				.buildBatches(jadxDecompiler.getClasses())
				.stream()
				.flatMap(Collection::stream)
				.map(JavaClass::getClassNode)
				.collect(Collectors.toList());
		decompileAndCheck(sortedClsNodes);
		return sortedClsNodes;
	}

	@NotNull
	public ClassNode searchTestCls(List<ClassNode> list, String shortClsName) {
		return searchCls(list, getTestPkg() + '.' + shortClsName);
	}

	@NotNull
	public ClassNode searchCls(List<ClassNode> list, String clsName) {
		for (ClassNode cls : list) {
			if (cls.getClassInfo().getFullName().equals(clsName)) {
				return cls;
			}
		}
		for (ClassNode cls : list) {
			if (cls.getClassInfo().getShortName().equals(clsName)) {
				return cls;
			}
		}
		fail("Class not found by name " + clsName + " in list: " + list);
		return null;
	}

	protected JadxDecompiler loadFiles(List<File> inputFiles) {
		args.setInputFiles(inputFiles);
		boolean useDx = !isJavaInput();
		LOG.info(useDx ? "Using dex input" : "Using java input, current java version: " + JavaUtils.JAVA_VERSION_INT);
		args.setUseDxInput(useDx);

		JadxDecompiler d = new JadxDecompiler(args);
		try {
			insertResources(d);
			d.load();
			return d;
		} catch (Exception e) {
			LOG.error("Load failed", e);
			d.close();
			fail(e.getMessage());
			return null;
		}
	}

	protected void decompileAndCheck(ClassNode cls) {
		decompileAndCheck(Collections.singletonList(cls));
	}

	protected void decompileAndCheck(List<ClassNode> clsList) {
		clsList.forEach(cls -> cls.add(AFlag.DONT_UNLOAD_CLASS)); // keep error and warning attributes
		clsList.forEach(ClassNode::decompile);

		for (ClassNode cls : clsList) {
			System.out.println("-----------------------------------------------------------");
			ICodeInfo code = cls.getCode();
			if (printLineNumbers) {
				printCodeWithLineNumbers(code);
			} else if (printOffsets) {
				printCodeWithOffsets(code);
			} else {
				System.out.println(code);
			}
		}
		System.out.println("-----------------------------------------------------------");
		if (printDisassemble) {
			clsList.forEach(this::printDisasm);
		}
		runChecks(clsList);
	}

	public void runChecks(ClassNode cls) {
		runChecks(Collections.singletonList(cls));
	}

	protected void runChecks(List<ClassNode> clsList) {
		clsList.forEach(cls -> checkCode(cls, allowWarnInCode));
		compileClassNode(clsList);
		clsList.forEach(this::runAutoCheck);
	}

	private void printDisasm(ClassNode cls) {
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
		System.out.println(cls.getDisassembledCode());
		System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
	}

	private void printCodeWithLineNumbers(ICodeInfo code) {
		String codeStr = code.getCodeStr();
		Map<Integer, Integer> lineMapping = code.getCodeMetadata().getLineMapping();
		String[] lines = codeStr.split("\\R");
		for (int i = 0; i < lines.length; i++) {
			String line = lines[i];
			int curLine = i + 1;
			String lineNumStr = "/* " + leftPad(String.valueOf(curLine), 3) + " */";
			Integer sourceLine = lineMapping.get(curLine);
			if (sourceLine != null) {
				lineNumStr += " /* " + sourceLine + " */";
			}
			System.out.println(rightPad(lineNumStr, 20) + line);
		}
	}

	private void printCodeWithOffsets(ICodeInfo code) {
		String codeStr = code.getCodeStr();
		ICodeMetadata metadata = code.getCodeMetadata();
		int lineStartPos = 0;
		String newLineStr = args.getCodeNewLineStr();
		int newLineLen = newLineStr.length();
		for (String line : codeStr.split(newLineStr)) {
			Object ann = metadata.getAt(lineStartPos);
			String offsetStr = "";
			if (ann instanceof InsnCodeOffset) {
				int offset = ((InsnCodeOffset) ann).getOffset();
				offsetStr = "/* " + leftPad(String.valueOf(offset), 5) + " */";
			}
			System.out.println(rightPad(offsetStr, 12) + line);
			lineStartPos += line.length() + newLineLen;
		}
	}

	/**
	 * Insert mock resource table data ('.arsc' file)
	 */
	private void insertResources(JadxDecompiler decompiler) throws IOException {
		if (resMap.isEmpty()) {
			return;
		}
		String resTableName = "test-res-table";
		Path resTablePath = testDir.resolve(resTableName);
		File resTableFile = resTablePath.toFile().getAbsoluteFile();
		FileUtils.makeDirsForFile(resTableFile);
		Files.writeString(resTablePath, resTableName);
		JadxArgs jadxArgs = decompiler.getArgs();
		jadxArgs.getInputFiles().add(resTableFile);

		// load mock file as 'arsc'
		decompiler.addCustomResourcesLoader(new CustomResourcesLoader() {
			@Override
			public boolean load(ResourcesLoader loader, List<ResourceFile> list, File file) {
				if (file == resTableFile) {
					list.add(ResourceFile.createResourceFile(decompiler, resTableFile, ResourceType.ARSC));
					return true;
				}
				return false;
			}

			@Override
			public void close() {
			}
		});

		// convert resources map to resource storage object
		ResourceStorage resStorage = new ResourceStorage(jadxArgs.getSecurity());
		for (Map.Entry<Integer, String> entry : resMap.entrySet()) {
			Integer id = entry.getKey();
			String name = entry.getValue();
			String[] parts = name.split("\\.");
			resStorage.add(new ResourceEntry(id, "", parts[0], parts[1], ""));
		}

		// mock res table parser to just return resource storage
		IResTableParser resTableParser = new IResTableParser() {
			@Override
			public void decode(InputStream inputStream) {
			}

			@Override
			public ResourceStorage getResStorage() {
				return resStorage;
			}

			@Override
			public ResContainer decodeFiles() {
				return ResContainer.textResource(resTableName, new SimpleCodeInfo(resTableName));
			}

			@Override
			public BinaryXMLStrings getStrings() {
				return new BinaryXMLStrings();
			}
		};

		// directly return generated resource storage instead parsing for mock res file
		decompiler.getResourcesLoader().addResTableParserProvider(resFile -> {
			if (resFile.getOriginalName().equals(resTableFile.getAbsolutePath())) {
				return resTableParser;
			}
			return null;
		});
	}

	private void runAutoCheck(ClassNode cls) {
		String clsName = cls.getClassInfo().getRawName().replace('/', '.');
		try {
			// run 'check' method from original class
			boolean sourceCheckFound = runSourceAutoCheck(clsName);

			// run 'check' method from decompiled class
			if (compile && (sourceCheckFound || forceDecompiledCheck)) {
				runDecompiledAutoCheck(cls);
			}
		} catch (Exception e) {
			LOG.error("Auto check failed", e);
			fail("Auto check exception: " + e.getMessage());
		}
	}

	private boolean runSourceAutoCheck(String clsName) {
		if (sourceCompiler == null) {
			System.out.println("Source check: no code");
			return false;
		}
		Class<?> origCls;
		try {
			origCls = sourceCompiler.getClass(clsName);
		} catch (ClassNotFoundException e) {
			rethrow("Missing class: " + clsName, e);
			return false;
		}
		Method checkMth;
		try {
			checkMth = sourceCompiler.getMethod(origCls, CHECK_METHOD_NAME, new Class[] {});
		} catch (NoSuchMethodException e) {
			// ignore
			return false;
		}
		if (!checkMth.getReturnType().equals(void.class)
				|| !Modifier.isPublic(checkMth.getModifiers())
				|| Modifier.isStatic(checkMth.getModifiers())) {
			fail("Wrong 'check' method");
			return false;
		}
		try {
			limitExecTime(() -> checkMth.invoke(origCls.getConstructor().newInstance()));
			System.out.println("Source check: PASSED");
			return true;
		} catch (Throwable e) {
			throw new JadxRuntimeException("Source check failed", e);
		}
	}

	public void runDecompiledAutoCheck(ClassNode cls) {
		try {
			limitExecTime(() -> invoke(decompiledCompiler, cls.getFullName(), CHECK_METHOD_NAME));
			System.out.println("Decompiled check: PASSED");
		} catch (Throwable e) {
			rethrow("Decompiled check failed", e);
		}
	}

	private <T> T limitExecTime(Callable<T> call) {
		ExecutorService executor = Executors.newSingleThreadExecutor();
		Future<T> future = executor.submit(call);
		try {
			return future.get(5, TimeUnit.SECONDS);
		} catch (TimeoutException ex) {
			future.cancel(true);
			rethrow("Execution timeout", ex);
		} catch (Throwable ex) {
			rethrow(ex.getMessage(), ex);
		} finally {
			executor.shutdownNow();
		}
		return null;
	}

	public static void rethrow(String msg, Throwable e) {
		if (e instanceof InvocationTargetException) {
			rethrow(msg, e.getCause());
		} else if (e instanceof ExecutionException) {
			rethrow(msg, e.getCause());
		} else if (e instanceof AssertionError) {
			System.err.println(msg);
			throw (AssertionError) e;
		} else {
			throw new RuntimeException(msg, e);
		}
	}

	protected MethodNode getMethod(ClassNode cls, String methodName) {
		for (MethodNode mth : cls.getMethods()) {
			if (mth.getName().equals(methodName)) {
				return mth;
			}
		}
		fail("Method not found " + methodName + " in class " + cls);
		return null;
	}

	protected FieldNode getField(ClassNode cls, String fieldName) {
		for (FieldNode fld : cls.getFields()) {
			if (fld.getName().equals(fieldName)) {
				return fld;
			}
		}
		fail("Field not found " + fieldName + " in class " + cls);
		return null;
	}

	void compileClassNode(List<ClassNode> clsList) {
		if (!compile) {
			return;
		}
		try {
			// TODO: eclipse uses files or compilation units providers added in Java 9
			compilerOptions.setUseEclipseCompiler(false);
			decompiledCompiler = new TestCompiler(compilerOptions);
			decompiledCompiler.compileNodes(clsList);
			System.out.println("Compilation: PASSED");
		} catch (Exception e) {
			fail(e);
		}
	}

	public Object invoke(TestCompiler compiler, String clsFullName, String method) throws Exception {
		assertThat(compiler).as("compiler not ready").isNotNull();
		return compiler.invoke(clsFullName, method, new Class<?>[] {}, new Object[] {});
	}

	private List<File> compileClass(Class<?> cls) throws IOException {
		File sourceFile = getSourceFileForClass(cls);
		List<File> clsFiles = compileSourceFiles(Collections.singletonList(sourceFile));
		if (removeParentClassOnInput) {
			// remove classes which are parents for test class
			String clsFullName = cls.getName();
			String clsName = clsFullName.substring(clsFullName.lastIndexOf('.') + 1);
			clsFiles.removeIf(next -> !next.getName().contains(clsName));
		}
		return clsFiles;
	}

	private File getSourceFileForClass(Class<?> cls) {
		String clsFullName = cls.getName();
		int innerEnd = clsFullName.indexOf('$');
		String rootClsName = innerEnd == -1 ? clsFullName : clsFullName.substring(0, innerEnd);
		String javaFileName = rootClsName.replace('.', '/') + ".java";
		File file = new File(TEST_DIRECTORY, javaFileName);
		if (file.exists()) {
			return file;
		}
		File file2 = new File(TEST_DIRECTORY2, javaFileName);
		if (file2.exists()) {
			return file2;
		}
		throw new JadxRuntimeException("Test source not found for class: " + clsFullName);
	}

	private List<File> compileSourceFiles(List<File> compileFileList) throws IOException {
		Path outTmp = Files.createTempDirectory(testDir, "jadx-tmp-classes");
		sourceCompiler = new TestCompiler(compilerOptions);
		List<File> files = sourceCompiler.compileFiles(compileFileList, outTmp);
		if (saveTestJar) {
			saveToJar(files, outTmp);
		}
		return files;
	}

	private void saveToJar(List<File> files, Path baseDir) throws IOException {
		Path jarFile = Files.createTempFile("tests-" + getTestName() + '-', ".jar");
		try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(jarFile))) {
			for (File file : files) {
				Path fullPath = file.toPath();
				Path relativePath = baseDir.relativize(fullPath);
				JarEntry entry = new JarEntry(relativePath.toString());
				jar.putNextEntry(entry);
				jar.write(Files.readAllBytes(fullPath));
				jar.closeEntry();
			}
		}
		LOG.info("Test jar saved to: {}", jarFile.toAbsolutePath());
	}

	public JadxArgs getArgs() {
		return args;
	}

	public CompilerOptions getCompilerOptions() {
		return compilerOptions;
	}

	public void setArgs(JadxArgs args) {
		this.args = args;
	}

	public void setResMap(Map<Integer, String> resMap) {
		this.resMap = resMap;
	}

	protected void noDebugInfo() {
		this.compilerOptions.setIncludeDebugInfo(false);
	}

	public void useEclipseCompiler() {
		Assumptions.assumeTrue(JavaUtils.checkJavaVersion(11), "eclipse compiler library using Java 11");
		this.compilerOptions.setUseEclipseCompiler(true);
	}

	public void useTargetJavaVersion(int version) {
		Assumptions.assumeTrue(JavaUtils.checkJavaVersion(version), "skip test for higher java version");
		this.compilerOptions.setJavaVersion(version);
	}

	protected void setFallback() {
		disableCompilation();
		this.args.setDecompilationMode(DecompilationMode.FALLBACK);
	}

	protected void disableCompilation() {
		this.compile = false;
	}

	protected void forceDecompiledCheck() {
		this.forceDecompiledCheck = true;
	}

	protected void enableDeobfuscation() {
		args.setDeobfuscationOn(true);
		args.setGeneratedRenamesMappingFileMode(GeneratedRenamesMappingFileMode.IGNORE);
		args.setDeobfuscationMinLength(2);
		args.setDeobfuscationMaxLength(64);
	}

	protected void allowWarnInCode() {
		allowWarnInCode = true;
	}

	protected void printLineNumbers() {
		printLineNumbers = true;
	}

	protected void printOffsets() {
		printOffsets = true;
	}

	public void useJavaInput() {
		this.useJavaInput = true;
	}

	public void useDexInput() {
		Assumptions.assumeFalse(USE_JAVA_INPUT, "skip dex input tests");
		this.useJavaInput = false;
	}

	public void useDexInput(String mode) {
		useDexInput();
		this.getArgs().getPluginOptions().put("java-convert.mode", mode);
	}

	protected boolean isJavaInput() {
		return Utils.getOrElse(useJavaInput, USE_JAVA_INPUT);
	}

	public void keepParentClassOnInput() {
		this.removeParentClassOnInput = false;
	}

	// Use only for debug purpose
	protected void printDisassemble() {
		this.printDisassemble = true;
	}

	// Use only for debug purpose
	protected void saveTestJar() {
		this.saveTestJar = true;
	}

	protected void addClsRename(String fullClsName, String newName) {
		JadxNodeRef clsRef = JadxNodeRef.forCls(fullClsName);
		getCodeData().getRenames().add(new JadxCodeRename(clsRef, newName));
	}

	protected void addMthRename(String fullClsName, String mthSignature, String newName) {
		JadxNodeRef mthRef = new JadxNodeRef(IJavaNodeRef.RefType.METHOD, fullClsName, mthSignature);
		getCodeData().getRenames().add(new JadxCodeRename(mthRef, newName));
	}

	protected void addFldRename(String fullClsName, String fldSignature, String newName) {
		JadxNodeRef fldRef = new JadxNodeRef(IJavaNodeRef.RefType.FIELD, fullClsName, fldSignature);
		getCodeData().getRenames().add(new JadxCodeRename(fldRef, newName));
	}

	protected JadxCodeData getCodeData() {
		JadxCodeData codeData = (JadxCodeData) getArgs().getCodeData();
		if (codeData == null) {
			codeData = new JadxCodeData();
			codeData.setRenames(new ArrayList<>());
			codeData.setComments(new ArrayList<>());
			getArgs().setCodeData(codeData);
		}
		return codeData;
	}

	protected JavaClass toJavaClass(ClassNode cls) {
		JavaClass javaClass = JadxInternalAccess.convertClassNode(jadxDecompiler, cls);
		assertThat(javaClass).isNotNull();
		return javaClass;
	}

	protected JavaMethod toJavaMethod(MethodNode mth) {
		JavaMethod javaMethod = JadxInternalAccess.convertMethodNode(jadxDecompiler, mth);
		assertThat(javaMethod).isNotNull();
		return javaMethod;
	}

	protected JavaVariable toJavaVariable(VarNode varNode) {
		JavaVariable javaVariable = (JavaVariable) jadxDecompiler.getJavaNodeByCodeAnnotation(null, varNode);
		assertThat(javaVariable).isNotNull();
		return javaVariable;
	}

	public File getResourceFile(String filePath) {
		URL resource = getClass().getClassLoader().getResource(filePath);
		assertThat(resource).as("Resource not found: %s", filePath).isNotNull();
		String resPath = resource.getFile();
		return new File(resPath);
	}
}
