Oscar Dieste

84 papers A* 1A 13B 2C 1Journal 35Unranked 28
YearRankTypeTitle / Venue / Authors
2025 J jnl
IET Softw.
Efraín R. Fonseca C., Marta López Fernández, Oscar Dieste, Natalia Juristo
2025 conf
CIbSE
Geovanny Raura, Efraín R. Fonseca C., Oscar Dieste
2025 J jnl
ACM Trans. Softw. Eng. Methodol.
Fernando Uyaguari, Silvia Teresita Acuña, John W. Castro, Davide Fucci, Oscar Dieste, Sira Vegas
2025 J jnl
Inf. Softw. Technol.
Fernando Uyaguari, Silvia Teresita Acuña, John W. Castro, Oscar Dieste, Natalia Juristo
2024 J jnl
CoRR
Alejandrina M. Aranda, Oscar Dieste, José Ignacio Panach, Natalia Juristo
2024 J jnl
CoRR
Oscar A. Testa, Efraín R. Fonseca C., Germán Montejano, Oscar Dieste
2024 ed.
REFSQ Workshops
Daniel Méndez, Ana Moreira, Jennifer Horkoff, Thorsten Weyer, Maya Daneva, Michael Unterkalmsteiner, Stan Bühne, Jennifer Hehn, Birgit Penzenstadler, Oscar Dieste, Nelly Condori-Fernández, Renata S. S. Guizzardi, Khan Mohammad Habibullah, Anna Perini, Angelo Susi, Sallam Abualhaija, Chetan Arora, Davide Dell'Anna, Alessio Ferrari, Sepideh Ghanavati, Fabiano Dalpiaz, Jan-Philipp Steghöfer, Alexander Rachmann, Jens Gulden, Andrea Müller, Martina Beck, Dominik Birkmeier, Andrea Herrmann, Patrick Mennig, Kurt Schneider
2024 J jnl
CoRR
Ranci Ren, John W. Castro, Santiago R. Acuña, Oscar Dieste, Silvia Teresita Acuña
2024 conf
REFSQ Workshops
Nelly Condori-Fernández, Oscar Dieste
2024 J jnl
CoRR
Fernando Uyaguari, Silvia Teresita Acuña, John W. Castro, Davide Fucci, Oscar Dieste, Sira Vegas
2024 J jnl
CoRR
Oscar Dieste, Ayse Tosun, Sira Vegas, Adrian Santos, Fernando Uyaguari, Jarno Kyykka, Natalia Juristo
2024 J jnl
CoRR
Ranci Ren, John W. Castro, Adrián Santos, Oscar Dieste, Silvia Teresita Acuña
2024 J jnl
CoRR
Alejandrina Aranda, Oscar Dieste, Natalia Juristo
2023 J jnl
IEEE Trans. Software Eng.
Alejandrina M. Aranda, Oscar Dieste, José Ignacio Panach, Natalia Juristo
2023 J jnl
IEEE Trans. Software Eng.
Juan M. Ferreira, Francy D. Rodríguez, Adrián Santos, Oscar Dieste, Silvia Teresita Acuña, Natalia Juristo
2023 J jnl
J. Syst. Softw.
Ranci Ren, John W. Castro, Santiago R. Acuña, Oscar Dieste, Silvia Teresita Acuña
2023 J jnl
IEEE Trans. Software Eng.
Ranci Ren, John W. Castro, Adrián Santos, Oscar Dieste, Silvia Teresita Acuña
2022 J jnl
IEEE Access
Ranci Ren, Mireya Zapata, John W. Castro, Oscar Dieste, Silvia Teresita Acuña
2022 A conf
EASE
Oscar Dieste, Sira Vegas
2022 J jnl
IEEE Access
Ranci Ren, Sara Pérez-Soler, John W. Castro, Oscar Dieste, Silvia Teresita Acuña
2021 J jnl
CoRR
Efraín R. Fonseca C., Oscar Dieste, Natalia Juristo
2021 J jnl
Empir. Softw. Eng.
Adrian Santos, Sira Vegas, Oscar Dieste, Fernando Uyaguari, Ayse Tosun, Davide Fucci, Burak Turhan, Giuseppe Scanniello, Simone Romano, Itir Karac, Marco Kuhrmann, Vladimir Mandic, Robert Ramac, Dietmar Pfahl, Christian Engblom, Jarno Kyykka, Kerli Rungi, Carolina Palomeque, Jaroslav Spisak, Markku Oivo, Natalia Juristo
2021 J jnl
IEEE Trans. Software Eng.
José Ignacio Panach, Oscar Dieste, Beatriz Marín, Sergio España, Sira Vegas, Oscar Pastor, Natalia Juristo
2021 J jnl
IEEE Trans. Software Eng.
Ayse Tosun, Oscar Dieste, Sira Vegas, Dietmar Pfahl, Kerli Rungi, Natalia Juristo
2021 J jnl
CoRR
Rolando P. Reyes, Oscar Dieste, Efraín R. Fonseca C., Natalia Juristo
2021 J jnl
CoRR
Oscar Dieste, Fernando Uyaguari, Natalia Juristo
2021 J jnl
CoRR
Valentina Lenarduzzi, Oscar Dieste, Davide Fucci, Sira Vegas
2021 A conf
ESEM
Valentina Lenarduzzi, Oscar Dieste, Davide Fucci, Sira Vegas
2020 J jnl
CoRR
Adrian Santos, Sira Vegas, Oscar Dieste, Fernando Uyaguari Uyaguari, Ayse Tosun, Davide Fucci, Burak Turhan, Giuseppe Scanniello, Simone Romano, Itir Karac, Marco Kuhrmann, Vladimir Mandic, Robert Ramac, Dietmar Pfahl, Christian Engblom, Jarno Kyykka, Kerli Rungi, Carolina Palomeque, Jaroslav Spisak, Markku Oivo, Natalia Juristo
2020 conf
REET@RE
Muhammad Laiq, Oscar Dieste
2020 J jnl
Inf. Softw. Technol.
Juan M. Ferreira, Silvia Teresita Acuña, Oscar Dieste, Sira Vegas, Adrian Santos, Francy D. Rodríguez, Natalia Juristo
2020 J jnl
CoRR
Adrian Santos, Sira Vegas, Fernando Uyaguari Uyaguari, Oscar Dieste, Burak Turhan, Natalia Juristo
2020 J jnl
Softw. Qual. J.
Adrian Santos, Sira Vegas, Fernando Uyaguari, Oscar Dieste, Burak Turhan, Natalia Juristo
2020 A conf
EASE
Rolando P. Reyes, Oscar Dieste, Efraín R. Fonseca C., Natalia Juristo
2020 conf
ICIT
Oscar A. Testa, Efraín R. Fonseca C., German Montejano, Narayan C. Debnath, Oscar Dieste
2019 conf
SCCC
Oscar A. Testa, Efraín R. Fonseca C., German Montejano, Oscar Dieste
2018 B conf
ICSSP
Oscar Dieste, Alejandrina Aranda, Fernando Uyaguari Uyaguari, Burak Turhan, Ayse Tosun, Davide Fucci, Markku Oivo, Natalia Juristo
2018 conf
CIbSE
Geovanny Raura, Efraín R. Fonseca C., John W. Castro, Tatiana Gualotuña, Rebeca Mejía, Mónica Santillán, Claudia Pons, Oscar Dieste
2018 A* conf
ICSE
Rolando P. Reyes, Oscar Dieste, Efraín R. Fonseca C., Natalia Juristo
2017 J jnl
Empir. Softw. Eng.
Ayse Tosun, Oscar Dieste, Davide Fucci, Sira Vegas, Burak Turhan, Hakan Erdogmus, Adrian Santos, Markku Oivo, Kimmo Toro, Janne Järvinen, Natalia Juristo
2017 J jnl
Inf. Softw. Technol.
Dante Carrizo Moreno, Oscar Dieste, Natalia Juristo
2017 J jnl
Empir. Softw. Eng.
Oscar Dieste, Alejandrina M. Aranda, Fernando Uyaguari Uyaguari, Burak Turhan, Ayse Tosun, Davide Fucci, Markku Oivo, Natalia Juristo
2017 conf
CIbSE
Oscar Dieste, Efraín R. Fonseca C., Jorge Raura, Alejandra Ponce
2017 A conf
RE
Xavier Franch, Daniel Méndez Fernández, Marc Oriol, Andreas Vogelsang, Rogardt Heldal, Eric Knauss, Guilherme Horta Travassos, Jeffrey C. Carver, Oscar Dieste, Thomas Zimmermann
2017 ed.
REFSQ Workshops
Eric Knauss, Angelo Susi, David Ameller, Daniel M. Berry, Fabiano Dalpiaz, Maya Daneva, Marian Daun, Oscar Dieste, Peter Forbrig, Eduard C. Groen, Andrea Herrmann, Jennifer Horkoff, Fitsum Meshesha Kifetew, Marite Kirikova, Alessia Knauss, Patrick Maeder, Fabio Massacci, Cristina Palomares, Jolita Ralyté, Ahmed Seffah, Alberto Siena, Bastian Tenbergen
2017 conf
CESI@ICSE
Oscar Dieste, Efraín R. Fonseca C., Geovanny Raura, Priscila Rodriguez
2016 J jnl
IEEE Trans. Software Eng.
Alejandrina M. Aranda, Oscar Dieste, Natalia Juristo
2016 A conf
ESEM
Jeffrey C. Carver, Oscar Dieste, Nicholas A. Kraft, David Lo, Thomas Zimmermann
2016 ed.
REFSQ Workshops
Elizabeth Bjarnason, Markus Borg, Marian Daun, Oscar Dieste, Sergio España, Michael Felderer, Peter Forbrig, Xavier Franch, Gregory Gay, Andrea Herrmann, Jennifer Horkoff, Marite Kirikova, Mirko Morandini, Andreas L. Opdahl, Barbara Paech, Cristina Palomares, Kai Petersen, Ahmed Seffah, Bastian Tenbergen, Michael Unterkalmsteiner
2016 conf
CIbSE
Oscar Dieste
2015 conf
CESI@ICSE
Sira Vegas, Oscar Dieste, Natalia Juristo Juzgado
2015 conf
CIbSE
Cecilia Hinojosa, Geovanny Raura, Efraín R. Fonseca C., Oscar Dieste
2015 conf
WER
Cecilia Hinojosa, Geovanny Raura, Efraín R. Fonseca C., Oscar Dieste
2015 J jnl
Inf. Softw. Technol.
Davide Fucci, Burak Turhan, Natalia Juristo Juzgado, Oscar Dieste, Ayse Tosun Misirli, Markku Oivo
2014 J jnl
Empir. Softw. Eng.
Cecilia Apa, Oscar Dieste, Edison G. Espinosa G., Efraín R. Fonseca C.
2014 A conf
EASE
Alejandrina Aranda, Oscar Dieste, Natalia Juristo Juzgado
2014 A conf
EASE
Maria Teresa Baldassarre, Jeffrey C. Carver, Oscar Dieste, Natalia Juristo Juzgado
2014 A conf
ESEM
Efraín R. Fonseca C., Oscar Dieste, Natalia Juristo Juzgado, Estefanía Serral, Stefan Biffl
2014 J jnl
Inf. Softw. Technol.
Dante Carrizo Moreno, Oscar Dieste, Natalia Juristo Juzgado
2014 conf
CESI
Ayse Tosun Misirli, Hakan Erdogmus, Natalia Juristo Juzgado, Oscar Dieste
2013 conf
Perspectives on the Future of Software Engineering
Oscar Dieste, Natalia Juristo
2013 conf
ICSOB
Hernán Merlino, Oscar Dieste, Patricia Pesado, Ramón García-Martínez
2013 A conf
ESEM
Stefan Biffl, Estefanía Serral, Dietmar Winkler, Nelly Condori-Fernández, Oscar Dieste, Natalia Juristo Juzgado
2013 conf
CESI@ICSE
Oscar Dieste, Natalia Juristo Juzgado, Mauro Danilo Martínez
2012 A conf
EASE
Silvia Teresita Acuña, John W. Castro, Oscar Dieste, Natalia Juristo Juzgado
2012 B ed.
PROFES
Oscar Dieste, Andreas Jedlitschka, Natalia Juristo Juzgado
2012 C conf
SEKE
Hernán Merlino, Oscar Dieste, Patricia Pesado, Ramón García-Martínez
2011 A conf
EASE
Oscar Dieste, Enrique Fernández, Ramón García-Martínez, Natalia Juristo
2011 conf
WER
Dante Carrizo Moreno, Oscar Dieste, Natalia Juristo Juzgado, Marta López
2011 conf
PROFES Workshops
Dante Carrizo Moreno, Oscar Dieste, Marta López
2011 A conf
ESEM
Oscar Dieste, Anna Grimán, Natalia Juristo Juzgado, Himanshu Saxena
2011 J jnl
IEEE Trans. Software Eng.
Oscar Dieste, Natalia Juristo Juzgado
2011 A conf
ESEM
Oscar Dieste, Enrique Fernández, Ramón García-Martínez, Natalia Juristo Juzgado
2010 ed.
WER
Graciela D. S. Hadad, Oscar Dieste, Juan Pablo Carvallo
2010 conf
WER
Oscar Dieste
2010 conf
CIbSE
Oscar Dieste, Enrique Fernández, Patricia Pesado, Ramón García-Martínez
2010 conf
WER
Graciela D. S. Hadad, Oscar Dieste
2009 conf
JISBD
Enrique Fernández, Oscar Dieste, Patricia Pesado, Ramón García-Martínez
2009 conf
WER
Nelly Condori-Fernández, Maya Daneva, Klaas Sikkel, Roel J. Wieringa, Oscar Dieste, Oscar Pastor
2008 conf
Advances in Information Systems Research, Education and Practice
Paola Britos, Oscar Dieste, Ramón García-Martínez
2008 conf
JISBD
Oscar Dieste, Anna Grimán, Marta López
2008 conf
WER
Dante Carrizo Moreno, Oscar Dieste, Natalia Juristo Juzgado
2008 conf
WER
Oscar Dieste, Marta López, Felicidad Ramos
2007 conf
CIbSE
Dante Carrizo Moreno, Oscar Dieste
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