Jacob Furst

83 papers A* 1B 17C 6Misc 4Journal 11Unranked 44
YearRankTypeTitle / Venue / Authors
2026 conf
BIOSTEC (4)
Ankita Mishra, Charmi Patel, Naveen Kumar Reddy Veeramreddy, Yiyang Wang, John Mazzeo, Jamshid Sourati, Kelly McCabe, Roselyne Tchoua, Jacob Furst, Daniela Raicu
2025 conf
eScience
Naveen Kumar Reddy Veeramreddy, Ankita Mishra, Navika Maglani, Sameer Shaik, Kelly McCabe, Jacob Furst, Daniela Stan Raicu, Roselyne Tchoua, Jamshid Sourati
2025 J jnl
CoRR
Sara Yavari, Jacob Furst
2023 B conf
CBMS
Arun Sivakumar, Yiyang Wang, Roselyne Tchoua, Thiruvarangan Ramaraj, Jacob Furst, Daniela Stan Raicu
2023 conf
Computer-Aided Diagnosis
Katherine Aubert, Catherine Huber, Jacob Furst, Daniela Stan Raicu, Roselyne Tchoua
2023 Misc conf
ICMLC
Crystal Contreras, Hristina Dokic, Zhen Huang, Daniela Stan Raicu, Jacob Furst, Roselyne Tchoua
2022 Misc conf
SAC
Jennifer Piane, Yiyang Wang, Xufan Ma, Jacob Furst, Daniela Stan Raicu
2022 J jnl
CoRR
Bowen Qiu, Daniela Raicu, Jacob Furst, Roselyne Tchoua
2022 conf
TransAI
Edward Xu, Thiruvarangan Ramaraj, Roselyne Tchoua, Jacob Furst, Daniela Raicu
2022 conf
ECCV Workshops (7)
Mirtha Lucas, Miguel Lerma, Jacob Furst, Daniela Raicu
2022 conf
Computer-Aided Diagnosis
Suhev Shakya, Mariana Vasquez, Yiyang Wang, Roselyne Tchoua, Jacob Furst, Daniela Raicu
2022 B conf
Image Processing
Mariana Vasquez, Suhev Shakya, Ian Wang, Jacob Furst, Roselyne Tchoua, Daniela Raicu
2022 B conf
ICPR
Yiyang Wang, Bowen Qiu, Thiruvarangan Ramaraj, Ilyas Ustun, Jacob Furst, Daniela Raicu
2022 C conf
ICMLA
Jennifer Schwabe, Chelsea Hua, Emma M. Allen, Leonard A. Jason, Jacob Furst, Daniela Raicu
2022 conf
ISVC (1)
Mirtha Lucas, Miguel Lerma, Jacob Furst, Daniela Raicu
2022 conf
Computer-Aided Diagnosis
Claire Weissman, Lilly Roelofs, Jacob Furst, Daniela Stan Raicu, Roselyne Tchoua
2022 Misc conf
SAC
Lilly Roelofs, Claire Weissman, Daniela Raicu, Jacob Furst, Roselyne Tchoua
2022 B conf
e-Science
Abigail Keller, Jacob Furst, Daniela Raicu, Peter M. Hastings, Roselyne Tchoua
2022 J jnl
CoRR
Mirtha Lucas, Miguel Lerma, Jacob Furst, Daniela Raicu
2021 conf
BIBM
Amal N. Almansour, Jacob Furst, Daniela Raicu, Enid Montague
2021 B conf
Image Processing
Abum Okemgbo, David Rein, Yiyang Wang, Amani A. Fawzi, Jacob Furst, Daniela Raicu
2021 conf
ICCS (1)
Erin Murphy, Alexander Rasin, Jacob Furst, Daniela Raicu, Roselyne Tchoua
2021 B conf
e-Science
Erin Murphy, Alexander Rasin, Jacob Furst, Daniela Raicu, Roselyne Tchoua
2020 B conf
CBMS
Mengyuan Shang, Jacob Furst, Daniela Stan Raicu
2020 conf
AICCC
Arun Gopal Govindaswamy, Enid Montague, Daniela Stan Raicu, Jacob Furst
2020 conf
Computer-Aided Diagnosis
Yiyang Wang, Xufan Ma, Rob Weddell, Abum Okemgbo, David Rein, Amani A. Fawzi, Jacob Furst, Daniela Raicu
2020 conf
HCI (18)
Tianyi Tan, Enid Montague, Jacob Furst, Daniela Raicu
2020 B conf
CBMS
Priya Deshpande, Alexander Rasin, Roselyne Tchoua, Jacob Furst, Daniela Raicu, Sameer K. Antani
2020 C conf
BIBE
Yiyang Wang, Mirtha Lucas, Jacob Furst, Amani A. Fawzi, Daniela Raicu
2020 C conf
BIBE
Mirtha Lucas, Miguel Lerma, Jacob Furst, Daniela Raicu
2020 conf
EMBC
Bowen Qiu, Jacob Furst, Alexander Rasin, Roselyne Tchoua, Daniela Raicu
2020 J jnl
J. Digit. Imaging
Priya Deshpande, Alexander Rasin, Jun Son, Sungmin Kim, Eli T. Brown, Jacob Furst, Daniela Stan Raicu, Steven M. Montner, Samuel G. Armato
2020 Misc conf
IVCNZ
Arun G. Govindaswamy, Enid Montague, Daniela Raicu, Jacob Furst
2020 C conf
BIBE
Tianyi Tan, Enid Montague, Jacob Furst, Daniela Raicu
2019 conf
BHI
Jennifer Piane, Yiyang Wang, Timothy Cheung, Hongkyun Kim, Jacob Furst, Daniela Raicu
2019 conf
Computer-Aided Diagnosis
Chufan Gao, Stephen Clark, Jacob Furst, Daniela Raicu
2019 J jnl
Data
Priya Deshpande, Alexander Rasin, Jacob Furst, Daniela Raicu, Sameer K. Antani
2019 conf
Computer-Aided Diagnosis
Samuel Berglin, Eura Shin, Jacob Furst, Daniela Raicu
2019 conf
MLDM (2)
Eura Shin, Samuel Berglin, Jacob Furst, Daniela Raicu
2019 C conf
BIBE
Daniel Gutstein, Enid Montague, Jacob Furst, Daniela Raicu
2019 conf
ICIAR (2)
Yan Yu, Yiyang Wang, Jacob Furst, Daniela Raicu
2019 conf
KDIR
Priya Deshpande, Alexander Rasin, Fang Cao, Sriram Yarlagadda, Eli T. Brown, Jacob Furst, Daniela Stan Raicu
2019 conf
BIBM
Daniel Gutstein, Enid Montague, Jacob Furst, Daniela Raicu
2018 J jnl
Int. J. Knowl. Discov. Bioinform.
Priya Deshpande, Alexander Rasin, Eli T. Brown, Jacob Furst, Steven M. Montner, Samuel G. Armato III, Daniela Stan Raicu
2017 conf
MIH@KDD
Priya Deshpande, Alexander Rasin, Eli T. Brown, Jacob Furst, Daniela Raicu, Steven M. Montner, Samuel G. Armato III
2016 J jnl
Comput. Math. Methods Medicine
Huang Li, Hongkyun Kim, Jacob Furst, Daniela Raicu
2016 conf
BIBM
Yiyang Wang, Carleton Smith, Mingfei Shao, Li Huang, Jacob Furst, Daniela Raicu, Hongkyun Kim
2016 conf
BIBM
Mohamed El-Dirany, Forrest Wang, Jacob Furst, John Rogers, Daniela Raicu
2016 conf
Computer-Aided Diagnosis
Miguel Carrazza, Brendan Kennedy, Alexander Rasin, Jacob Furst, Daniela Raicu
2015 conf
Computer-Aided Diagnosis
Elza Margolin, Jacob Furst, Daniela Raicu
2015 conf
ICDM Workshops
Brendan Kennedy, Miguel Carrazza, Alex Rasin, Jacob Furst, Daniela Stan Raicu
2015 B conf
Image Processing
Yu Zhang, Noriko Tomuro, Jacob Furst, Daniela Stan Raicu
2015 conf
Computer-Aided Diagnosis
Amelia Riely, Kyle Sablan, Thomas Xiaotao, Jacob Furst, Daniela Raicu
2015 J jnl
J. Digit. Imaging
Ronald Niehaus, Daniela Stan Raicu, Jacob Furst, Samuel G. Armato III
2014 C conf
ICMLA
Mayra Alejandra Molina Puentes, Jacob Furst, Daniela Stan Raicu
2014 conf
ICDM Workshops
Mike Seidel, Alexander Rasin, Jacob Furst, Daniela Stan Raicu
2013 conf
MLDM
Sydney Williams, Michael Harris, Jacob Furst, Daniela Stan Raicu
2013 conf
Intelligent Robots and Computer Vision: Algorithms and Techniques
Jennifer Piane, Daniela Raicu, Jacob Furst
2013 conf
ICMLA (2)
Ethan Smith, Patrick Stein, Jacob Furst, Daniela Stan Raicu
2012 J jnl
Int. J. Comput. Assist. Radiol. Surg.
Yu Zhang, Noriko Tomuro, Jacob Furst, Daniela Stan Raicu
2012 J jnl
J. Digit. Imaging
Dmitry Zinovev, Yujie Duo, Daniela Stan Raicu, Jacob Furst, Samuel G. Armato III
2012 B conf
ICIP
Anne-Marie Giuca, Kerry A. Seitz, Jacob Furst, Daniela Stan Raicu
2012 conf
Computer-Aided Diagnosis
Kerry A. Seitz, Anne-Marie Giuca, Jacob Furst, Daniela Raicu
2011 conf
ICIAR (2)
Olga Zinoveva, Dmitry Zinovev, Stephen A. Siena, Daniela Stan Raicu, Jacob Furst, Samuel G. Armato III
2011 conf
Computer-Aided Diagnosis
Jacob V. Gardner, Daniela Raicu, Jacob Furst
2011 conf
ICMLA (2)
Dmitry Zinovev, Jacob Furst, Daniela Stan Raicu
2011 B conf
Image Processing
Yu Zhang, Noriko Tomuro, Jacob Furst, Daniela Stan Raicu
2011 conf
EMBC
Dmitry Zinovev, Jonathan Feigenbaum, Jacob Furst, Daniela Raicu
2010 conf
Computer-Aided Diagnosis
Stephen A. Siena, Olga Zinoveva, Daniela Raicu, Jacob Furst, Samuel G. Armato
2010 B conf
Image Processing
Yu Zhang, Noriko Tomuro, Jacob Furst, Daniela Stan Raicu
2009 conf
FGIT-SIP
Jonathan Mortensen, Jia Jie Wu, Jacob Furst, John Rogers, Daniela Raicu
2009 J jnl
Algorithms
Dmitry Zinovev, Daniela Stan Raicu, Jacob Furst, Samuel G. Armato III
2009 conf
MCBR-CDS
Yu Zhang, Noriko Tomuro, Jacob Furst, Daniela Stan Raicu
2008 B conf
Image Processing
Ruchaneewan Susomboon, Daniela Raicu, Jacob Furst, Timothy Ben Johnson
2008 conf
BIOTECHNO
Ekarin Varutbangkul, Vesna Mitrovic, Daniela Stan Raicu, Jacob Furst
2008 conf
BIOTECHNO
Carl Philips, Daniel Li, Daniela Stan Raicu, Jacob Furst
2007 B conf
Image Processing
Mailan Pham, Ruchaneewan Susomboon, Tim Disney, Daniela Raicu, Jacob Furst
2007 conf
VISAPP (1)
Sharon I. Greenblum, Max Krucoff, Jacob Furst, Daniela Stan Raicu
2007 conf
ICDM Workshops
William H. Horsthemke, Ekarin Varutbangkul, Daniela Stan Raicu, Jacob Furst
2007 B conf
Image Processing
Daniela Stan Raicu, Ekarin Varutbangkul, Janie G. Cisneros, Jacob Furst, David S. Channin, Samuel G. Armato
2006 A* conf
ICDM
Ruchaneewan Susomboon, Daniela Stan Raicu, Jacob Furst, David S. Channin
2006 B conf
Image Processing
Stefanie Handrick, Bahare Naimipour, Daniela Raicu, Jacob Furst
2005 B conf
CBMS
Winnie Tsang, Andrew Corboy, Ken Lee, Daniela Stan Raicu, Jacob Furst
tests/unit/test_apk_process_method.py
← Index tests/unit/test_apk_process_method.py python
"""Mock-based tests for APKCodeAnalyzer._process_method and androguard integration.

Tests the boundary between our code and androguard objects, covering:
- xref extraction (get_xref_from / get_xref_to) for both 4.x and 3.x APIs
- external method skipping
- library method filtering
- smali lookup and androguard fallback disassembly
- deduplication of content hashes
- full output population (all result dict keys)
- minimum instruction filtering
"""
from dataclasses import dataclass
from typing import List
from unittest.mock import MagicMock, Mock, patch, PropertyMock

import pytest

pytestmark = [pytest.mark.unit, pytest.mark.apk, pytest.mark.decompile]


# ---------------------------------------------------------------------------
# Helpers: build mock androguard objects
# ---------------------------------------------------------------------------

SAMPLE_SMALI_BODY = """\
.locals 2
const/4 v0, 0x0
invoke-virtual {p0, v0}, Lcom/example/Foo;->bar(I)V
iget-object v1, p0, Lcom/example/Foo;->name:Ljava/lang/String;
add-int v0, v0, v1
return-void
"""

SAMPLE_JAVA_SOURCE = """\
public void doStuff() {
    int x = 0;
    bar(x);
    String n = this.name;
    x = x + n;
}
"""


def _make_encoded_method(
    class_name="Lcom/example/Foo;",
    method_name="doStuff",
    descriptor="()V",
    code=None,
):
    """Create a mock EncodedMethod."""
    enc = MagicMock()
    enc.get_class_name.return_value = class_name
    enc.get_name.return_value = method_name
    enc.get_descriptor.return_value = descriptor
    enc.get_code.return_value = code
    return enc


def _make_xref_entry_4x(class_name, method_name, offset=0):
    """Create a mock xref tuple in androguard 4.x format.

    4.x: (ClassAnalysis, MethodAnalysis, offset)
    MethodAnalysis has .get_method() returning the EncodedMethod.
    """
    ref_class = MagicMock()
    ref_method = MagicMock()
    inner_enc = MagicMock()
    inner_enc.get_class_name.return_value = class_name
    inner_enc.get_name.return_value = method_name
    ref_method.get_method.return_value = inner_enc
    # Remove direct get_class_name/get_name to simulate 4.x behavior
    del ref_method.get_class_name
    del ref_method.get_name
    return (ref_class, ref_method, offset)


def _make_xref_entry_3x(class_name, method_name, offset=0):
    """Create a mock xref tuple in androguard 3.x format.

    3.x: (ClassAnalysis, EncodedMethod, offset)
    EncodedMethod has .get_class_name() and .get_name() directly.
    """
    ref_class = MagicMock()
    ref_method = MagicMock(spec=[
        "get_class_name", "get_name",
    ])
    ref_method.get_class_name.return_value = class_name
    ref_method.get_name.return_value = method_name
    return (ref_class, ref_method, offset)


def _make_method_analysis(
    encoded=None,
    is_external=False,
    xref_from=None,
    xref_to=None,
):
    """Create a mock MethodAnalysis."""
    method = MagicMock()
    method.is_external.return_value = is_external
    method.get_method.return_value = encoded or _make_encoded_method()
    method.get_xref_from.return_value = xref_from or []
    method.get_xref_to.return_value = xref_to or []
    return method


def _make_analyzer(min_instructions=1):
    """Create an APKCodeAnalyzer with mocked dependencies."""
    with patch(
        "redb.extractors.decompiler.apk.analyzer.JADXDecompiler"
    ), patch(
        "redb.extractors.decompiler.apk.analyzer.ApktoolDisassembler"
    ):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        analyzer = APKCodeAnalyzer("/fake/path.apk")
        analyzer.min_instructions = min_instructions
        analyzer.library_filter = MagicMock()
        analyzer.library_filter.is_library.return_value = False
        return analyzer


def _empty_results():
    """Create a fresh empty results dict."""
    return {
        "decompiled_content": [],
        "decompiled_refs": [],
        "smali_content": [],
        "smali_refs": [],
        "similarity_metrics": [],
        "cfg": [],
        "analysis_errors": [],
    }


def _make_smali_method(body=SAMPLE_SMALI_BODY, instruction_count=5):
    """Create a mock SmaliMethod."""
    m = MagicMock()
    m.body = body
    m.instruction_count = instruction_count
    m.register_count = 2
    return m


# ---------------------------------------------------------------------------
# Tests: xref extraction
# ---------------------------------------------------------------------------

class TestXrefExtraction:
    """Tests that xrefs are correctly extracted from androguard objects."""

    def test_xref_from_androguard_4x(self):
        """4.x: ref_method.get_method().get_class_name() path."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        xref_from = [
            _make_xref_entry_4x("Lcom/example/Caller;", "init", 42),
            _make_xref_entry_4x("Lcom/example/Other;", "run", 100),
        ]
        method = _make_method_analysis(xref_from=xref_from)

        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )

        assert len(results["decompiled_refs"]) == 1
        ref = results["decompiled_refs"][0]
        assert ref["functions_caller"] == [
            "Lcom/example/Caller;->init",
            "Lcom/example/Other;->run",
        ]

    def test_xref_to_androguard_4x(self):
        """4.x: callees extracted via get_method() unwrap."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        xref_to = [
            _make_xref_entry_4x("Lcom/example/Dep;", "calculate", 10),
        ]
        method = _make_method_analysis(xref_to=xref_to)

        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )

        ref = results["decompiled_refs"][0]
        assert ref["functions_call"] == ["Lcom/example/Dep;->calculate"]

    def test_xref_from_androguard_3x_fallback(self):
        """3.x: ref_method.get_class_name() direct access fallback."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        xref_from = [
            _make_xref_entry_3x("Lcom/example/OldCaller;", "legacy"),
        ]
        method = _make_method_analysis(xref_from=xref_from)

        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )

        ref = results["decompiled_refs"][0]
        assert ref["functions_caller"] == ["Lcom/example/OldCaller;->legacy"]

    def test_xref_to_androguard_3x_fallback(self):
        """3.x: callees via direct access fallback."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        xref_to = [
            _make_xref_entry_3x("Lcom/example/OldDep;", "compute"),
        ]
        method = _make_method_analysis(xref_to=xref_to)

        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )

        ref = results["decompiled_refs"][0]
        assert ref["functions_call"] == ["Lcom/example/OldDep;->compute"]

    def test_empty_xrefs(self):
        """No xrefs produces empty lists, not None."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        method = _make_method_analysis()
        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )

        ref = results["decompiled_refs"][0]
        assert ref["functions_caller"] == []
        assert ref["functions_call"] == []

    def test_xref_exception_does_not_crash(self):
        """If get_xref_from() itself throws, method still processes."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        method = _make_method_analysis()
        method.get_xref_from.side_effect = RuntimeError("xref table corrupt")
        method.get_xref_to.side_effect = RuntimeError("xref table corrupt")

        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )

        # Method should still be processed with empty xrefs
        assert len(results["smali_refs"]) == 1
        ref = results["decompiled_refs"][0]
        assert ref["functions_caller"] == []
        assert ref["functions_call"] == []

    def test_multiple_xrefs(self):
        """Multiple callers and callees are all captured."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        xref_from = [
            _make_xref_entry_4x("Lcom/A;", "a1"),
            _make_xref_entry_4x("Lcom/B;", "b1"),
            _make_xref_entry_4x("Lcom/C;", "c1"),
        ]
        xref_to = [
            _make_xref_entry_4x("Lcom/D;", "d1"),
            _make_xref_entry_4x("Lcom/E;", "e1"),
        ]
        method = _make_method_analysis(xref_from=xref_from, xref_to=xref_to)

        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )

        ref = results["decompiled_refs"][0]
        assert len(ref["functions_caller"]) == 3
        assert len(ref["functions_call"]) == 2


# ---------------------------------------------------------------------------
# Tests: method filtering and skipping
# ---------------------------------------------------------------------------

class TestMethodFiltering:
    """Tests for external/library/min-instruction filtering."""

    def test_external_method_skipped(self):
        """External methods (no code body) produce no output."""
        analyzer = _make_analyzer()
        results = _empty_results()

        method = _make_method_analysis(is_external=True)

        analyzer._process_method(
            method, {}, {}, results, set(), set(),
        )

        assert all(len(v) == 0 for v in results.values())

    def test_library_method_skipped(self):
        """Library methods produce no output."""
        analyzer = _make_analyzer()
        analyzer.library_filter.is_library.return_value = True
        results = _empty_results()

        method = _make_method_analysis()

        analyzer._process_method(
            method, {}, {}, results, set(), set(),
        )

        assert all(len(v) == 0 for v in results.values())

    def test_min_instruction_filter(self):
        """Methods below min_instructions threshold are skipped."""
        analyzer = _make_analyzer(min_instructions=10)
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        method = _make_method_analysis()
        # Only 3 instructions — below threshold of 10
        smali_methods = {method_key: _make_smali_method(instruction_count=3)}

        analyzer._process_method(
            method, smali_methods, {}, results, set(), set(),
        )

        assert all(len(v) == 0 for v in results.values())

    def test_method_above_min_instructions_processed(self):
        """Methods at or above threshold are processed."""
        analyzer = _make_analyzer(min_instructions=5)
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        method = _make_method_analysis()
        smali_methods = {method_key: _make_smali_method(instruction_count=5)}

        analyzer._process_method(
            method, smali_methods, {}, results, set(), set(),
        )

        assert len(results["smali_content"]) == 1
        assert len(results["smali_refs"]) == 1


# ---------------------------------------------------------------------------
# Tests: smali body lookup and androguard fallback
# ---------------------------------------------------------------------------

class TestSmaliLookup:
    """Tests for smali body lookup from apktool and androguard fallback."""

    def test_apktool_smali_used_when_available(self):
        """When apktool smali is available, it's used."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        method = _make_method_analysis()
        smali_methods = {method_key: _make_smali_method()}

        analyzer._process_method(
            method, smali_methods, {}, results, set(), set(),
        )

        assert len(results["smali_content"]) == 1
        assert results["smali_content"][0]["smali_method"] == SAMPLE_SMALI_BODY

    def test_androguard_fallback_when_no_apktool(self):
        """When apktool smali is missing, _disassemble_with_androguard is used."""
        analyzer = _make_analyzer()
        results = _empty_results()

        # Create encoded method with code
        instruction1 = MagicMock()
        instruction1.get_name.return_value = "const/4"
        instruction1.get_output.return_value = "v0, 0x0"
        instruction2 = MagicMock()
        instruction2.get_name.return_value = "invoke-virtual"
        instruction2.get_output.return_value = "v0, Lcom/Foo;->bar()V"
        instruction3 = MagicMock()
        instruction3.get_name.return_value = "add-int"
        instruction3.get_output.return_value = "v0, v1, v2"
        instruction4 = MagicMock()
        instruction4.get_name.return_value = "sub-int"
        instruction4.get_output.return_value = "v3, v0, v1"
        instruction5 = MagicMock()
        instruction5.get_name.return_value = "return-void"
        instruction5.get_output.return_value = ""

        code = MagicMock()
        code.get_registers_size.return_value = 4
        bytecode = MagicMock()
        bytecode.get_instructions.return_value = [
            instruction1, instruction2, instruction3, instruction4, instruction5,
        ]
        code.get_bc.return_value = bytecode

        encoded = _make_encoded_method(code=code)
        method = _make_method_analysis(encoded=encoded)

        # Empty smali_methods — forces androguard fallback
        analyzer._process_method(
            method, {}, {}, results, set(), set(),
        )

        assert len(results["smali_content"]) == 1
        body = results["smali_content"][0]["smali_method"]
        assert "const/4" in body
        assert "return-void" in body

    def test_androguard_fallback_no_code(self):
        """When encoded method has no code, method is skipped."""
        analyzer = _make_analyzer()
        results = _empty_results()

        encoded = _make_encoded_method(code=None)
        method = _make_method_analysis(encoded=encoded)

        analyzer._process_method(
            method, {}, {}, results, set(), set(),
        )

        assert all(len(v) == 0 for v in results.values())

    def test_androguard_fallback_bytecode_exception(self):
        """When bytecode iteration throws, method is skipped gracefully."""
        analyzer = _make_analyzer()
        results = _empty_results()

        code = MagicMock()
        code.get_registers_size.return_value = 2
        bytecode = MagicMock()
        bytecode.get_instructions.side_effect = RuntimeError("bad bytecode")
        code.get_bc.return_value = bytecode

        encoded = _make_encoded_method(code=code)
        method = _make_method_analysis(encoded=encoded)

        analyzer._process_method(
            method, {}, {}, results, set(), set(),
        )

        # Should not crash, but no content produced
        assert len(results["smali_content"]) == 0


# ---------------------------------------------------------------------------
# Tests: output population and deduplication
# ---------------------------------------------------------------------------

class TestOutputPopulation:
    """Tests that all result dict keys are correctly populated."""

    def _process_standard_method(self, with_java=True):
        """Helper: process a standard method and return results."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"

        xref_from = [_make_xref_entry_4x("Lcom/Caller;", "call")]
        xref_to = [_make_xref_entry_4x("Lcom/Dep;", "dep")]
        method = _make_method_analysis(
            xref_from=xref_from, xref_to=xref_to
        )

        smali_methods = {method_key: _make_smali_method()}
        java_methods = {}
        if with_java:
            java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, set(), set(),
        )
        return results

    def test_smali_content_populated(self):
        results = self._process_standard_method()
        assert len(results["smali_content"]) == 1
        entry = results["smali_content"][0]
        assert "smali_method_hash" in entry
        assert entry["smali_method"] == SAMPLE_SMALI_BODY
        assert entry["smali_method_type"] == "USER"
        assert entry["smali_instructions_count"] == 5
        assert isinstance(entry["smali_has_string_encryption"], bool)
        assert isinstance(entry["smali_has_reflection_calls"], bool)
        assert isinstance(entry["smali_excessive_goto_count"], bool)

    def test_smali_refs_populated(self):
        results = self._process_standard_method()
        assert len(results["smali_refs"]) == 1
        ref = results["smali_refs"][0]
        assert "smali_method_hash" in ref
        assert ref["smali_class_name"] == "com.example.Foo"
        assert ref["smali_method_name"] == "doStuff"
        assert ref["smali_method_signature"] == "()V"

    def test_decompiled_content_populated_with_java(self):
        results = self._process_standard_method(with_java=True)
        assert len(results["decompiled_content"]) == 1
        entry = results["decompiled_content"][0]
        assert "decompiled_method_hash" in entry
        assert entry["decompiled_method"] == SAMPLE_JAVA_SOURCE

    def test_decompiled_refs_populated_with_java(self):
        results = self._process_standard_method(with_java=True)
        assert len(results["decompiled_refs"]) == 1
        ref = results["decompiled_refs"][0]
        assert "decompiled_method_hash" in ref
        assert "smali_method_hash" in ref
        assert ref["decompiled_class_name"] == "com.example.Foo"
        assert ref["decompiled_method_name"] == "doStuff"
        assert "decompiled_method_prototype" in ref
        assert ref["functions_caller"] == ["Lcom/Caller;->call"]
        assert ref["functions_call"] == ["Lcom/Dep;->dep"]

    def test_no_decompiled_without_java(self):
        results = self._process_standard_method(with_java=False)
        assert len(results["decompiled_content"]) == 0
        assert len(results["decompiled_refs"]) == 0
        # Smali should still be there
        assert len(results["smali_content"]) == 1

    def test_similarity_metrics_populated(self):
        results = self._process_standard_method()
        assert len(results["similarity_metrics"]) == 1
        sim = results["similarity_metrics"][0]
        assert "smali_method_hash" in sim
        assert "cyclomatic_complexity" in sim
        assert "minhash" in sim
        assert isinstance(sim["minhash"], list)
        # block_count moved to cfg table
        assert "block_count" not in sim

    def test_cfg_entry_populated(self):
        results = self._process_standard_method()
        assert len(results["cfg"]) == 1
        cfg = results["cfg"][0]
        assert "smali_method_hash" in cfg
        assert "block_count" in cfg
        assert "cfg_topology_hash" in cfg
        assert "prime_product_smali" in cfg
        assert "wl_minhash" in cfg
        assert "bb_features" in cfg
        assert isinstance(cfg["bb_features"], list)
        for feat in cfg["bb_features"]:
            assert len(feat) == 8

    def test_smali_deduplication(self):
        """Same smali body processed twice produces one content entry."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"
        seen_smali = set()

        method = _make_method_analysis()
        smali_methods = {method_key: _make_smali_method()}

        # Process same method twice
        analyzer._process_method(
            method, smali_methods, {}, results, set(), seen_smali,
        )
        analyzer._process_method(
            method, smali_methods, {}, results, set(), seen_smali,
        )

        # Content deduplicated, but refs are per-call
        assert len(results["smali_content"]) == 1
        assert len(results["smali_refs"]) == 2

    def test_decompiled_deduplication(self):
        """Same Java source processed twice produces one content entry."""
        analyzer = _make_analyzer()
        results = _empty_results()
        method_key = "Lcom/example/Foo;->doStuff()V"
        seen_decompiled = set()
        seen_smali = set()

        method = _make_method_analysis()
        smali_methods = {method_key: _make_smali_method()}
        java_methods = {"com.example.Foo.doStuff": SAMPLE_JAVA_SOURCE}

        analyzer._process_method(
            method, smali_methods, java_methods,
            results, seen_decompiled, seen_smali,
        )
        analyzer._process_method(
            method, smali_methods, java_methods,
            results, seen_decompiled, seen_smali,
        )

        assert len(results["decompiled_content"]) == 1
        assert len(results["decompiled_refs"]) == 2


# ---------------------------------------------------------------------------
# Tests: _disassemble_with_androguard
# ---------------------------------------------------------------------------

class TestDisassembleWithAndroguard:
    """Tests for the androguard bytecode disassembly fallback."""

    def test_no_code_returns_none(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        encoded = _make_encoded_method(code=None)
        body, count, regs = APKCodeAnalyzer._disassemble_with_androguard(encoded)
        assert body is None
        assert count == 0
        assert regs == 0

    def test_no_bytecode_returns_none(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        code = MagicMock()
        code.get_registers_size.return_value = 3
        code.get_bc.return_value = None
        encoded = _make_encoded_method(code=code)

        body, count, regs = APKCodeAnalyzer._disassemble_with_androguard(encoded)
        assert body is None
        assert count == 0
        assert regs == 3

    def test_basic_disassembly(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        instructions = []
        for name, output in [
            ("const/4", "v0, 0x0"),
            ("invoke-virtual", "v0, Lcom/Foo;->bar()V"),
            ("return-void", ""),
        ]:
            instr = MagicMock()
            instr.get_name.return_value = name
            instr.get_output.return_value = output
            instructions.append(instr)

        code = MagicMock()
        code.get_registers_size.return_value = 2
        bytecode = MagicMock()
        bytecode.get_instructions.return_value = instructions
        code.get_bc.return_value = bytecode

        encoded = _make_encoded_method(code=code)
        body, count, regs = APKCodeAnalyzer._disassemble_with_androguard(encoded)

        assert body is not None
        assert count == 3
        assert regs == 2
        assert "const/4" in body
        assert "return-void" in body

    def test_invoke_operands_normalized(self):
        """Androguard invoke output gets braces added around registers."""
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        instr = MagicMock()
        instr.get_name.return_value = "invoke-virtual"
        # Androguard format: no braces
        instr.get_output.return_value = "v0, v1, Lcom/Foo;->bar(I)V"

        code = MagicMock()
        code.get_registers_size.return_value = 2
        bytecode = MagicMock()
        bytecode.get_instructions.return_value = [instr]
        code.get_bc.return_value = bytecode

        encoded = _make_encoded_method(code=code)
        body, count, regs = APKCodeAnalyzer._disassemble_with_androguard(encoded)

        # Should be normalized to apktool format with braces
        assert "{v0, v1}" in body

    def test_bytecode_exception_returns_none(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer

        code = MagicMock()
        code.get_registers_size.return_value = 2
        bytecode = MagicMock()
        bytecode.get_instructions.side_effect = RuntimeError("corrupt")
        code.get_bc.return_value = bytecode

        encoded = _make_encoded_method(code=code)
        body, count, regs = APKCodeAnalyzer._disassemble_with_androguard(encoded)

        assert body is None
        assert regs == 2