Wei-Tsong Lee

77 papers A* 1B 8C 2Journal 31Unranked 35
YearRankTypeTitle / Venue / Authors
2023 conf
ICCE-Taiwan
Li-Chung Yeng, Wei-Tsong Lee, Hsin-Wen Wei
2019 conf
GCCE
Wei-Tsong Lee, Hsin-Wen Wei, Yao-Chiang Yang
2019 conf
ICCE-TW
I-Han Lee, Bo-Han Zhang, Wei-Tsong Lee, Hsin-Wen Wei
2019 conf
ICCE-TW
Chi-You Wei, Bo-Yi Li, Hsin-Wen Wei, Wei-Tsong Lee
2019 conf
ICCE-TW
Chun-Sheng Yang, Bo-Han Zhang, Hsin-Wen Wei, Wei-Tsong Lee
2017 conf
AINA Workshops
Chieh-Chi Chiu, Zhong-Xing Xie, Hsin-Wen Wei, Wei-Tsong Lee
2017 conf
ICC
Yingying Guo, Yan Sun, Tin-Yu Wu, Mohammad S. Obaidat, Wei-Tsong Lee
2015 J jnl
J. Inf. Hiding Multim. Signal Process.
Hsin-Wen Wei, Tin-Yu Wu, Wei-Tsong Lee, Che-Wei Hsu
2014 conf
IIH-MSP
Heng Yao Chang, Wei-Tsong Lee, Hsin-Wen Wei
2014 J jnl
Clust. Comput.
Tzu-Chi Huang, Kuo-Chih Chu, Wei-Tsong Lee, Yu-Sheng Ho
2014 conf
IIH-MSP
Jing Yue Qiu, Hsin-Wen Wei, Wei-Tsong Lee, Yu-Chang Lin
2014 conf
IIH-MSP
Wei Jyun Wong, Wei-Tsong Lee, Hsin-Wen Wei
2014 conf
ANT/SEIT
Tin Yu Wu, Wei-Tsong Lee, Chien-Yu Duan, Yu-Wei Wu
2014 conf
IIH-MSP
Chen Wei Chang, Wei-Tsong Lee, Hsin-Wen Wei
2014 J jnl
J. Netw. Comput. Appl.
Tin Yu Wu, Wei-Tsong Lee, Nadra Guizani, Tzu-Ming Wang
2014 J jnl
Knowl. Eng. Rev.
Chi-Yuan Chen, Tin-Yu Wu, Wei-Tsong Lee, Han-Chieh Chao, Jen-Chun Chiang
2014 conf
IIH-MSP
Yu-Chang Lin, Wei-Tsong Lee, Jing Yue Qiu
2014 conf
IIH-MSP
Tsung-Yuan Wu, Wei-Tsong Lee
2014 conf
ECC (2)
Yen-Liang Chen, Yao-Chiang Yang, Wei-Tsong Lee
2014 J jnl
Wirel. Pers. Commun.
Tin Yu Wu, Alagan Anpalagan, Wei-Tsong Lee, Ya-Chun Li
2013 conf
ICOIN
Tin Yu Wu, Wei-Tsong Lee, Jhih-Siang Huang, Chien-Yu Duan, Tain-Wen Suen
2013 J jnl
IEEE Wirel. Commun.
Tin Yu Wu, Sghaier Guizani, Wei-Tsong Lee, Po-Chang Huang
2013 conf
EMC/HumanCom
Ming-Zhi Wu, Yu-Chang Lin, Wei-Tsong Lee, Yu-Sun Lin, Fong-Hao Liu
2013 J jnl
Int. J. Technol. Hum. Interact.
Fong-Hao Liu, Hsiang-Fu Lo, Chun-Te Su, Der-Chyuan Lou, Wei-Tsong Lee
2013 J jnl
Int. J. Technol. Hum. Interact.
Wei-Tsong Lee, Tin Yu Wu, Yu-Chieh Cheng, Yue-Ru Chuang, Shiann-Tsong Sheu
2012 J jnl
Pers. Ubiquitous Comput.
Tin Yu Wu, Guan-Hsiung Liaw, Sing Wei Huang, Wei-Tsong Lee, Chung-Chi Wu
2012 J jnl
Int. J. Auton. Adapt. Commun. Syst.
I-Ju Liao, Tin Yu Wu, Kuo-Chih Chu, Wei-Tsong Lee
2012 conf
ISPACS
Kuo-Chih Chu, Tzu-Chi Huang, Jheng-Han Jhou, Wei-Tsong Lee
2012 conf
WTS
Tin-Yu Wu, Wei-Tsong Lee, Chia-Fan Lin
2012 J jnl
Comput. Commun.
Tin Yu Wu, Chi-Yuan Chen, Ling-Shang Kuo, Wei-Tsong Lee, Han-Chieh Chao
2012 conf
ICGEC
Ming-Zhi Wu, Wei-Tsong Lee, Ren-Ji Liao, George Chang
2012 J jnl
Comput. Commun.
Hsiang-Fu Lo, Sghaier Guizani, Tin Yu Wu, Wei-Tsong Lee, Hung-Wei Lin
2012 J jnl
Ad Hoc Networks
Tin Yu Wu, Sghaier Guizani, Wei-Tsong Lee, Kuo-Hung Liao
2012 J jnl
Concurr. Comput. Pract. Exp.
Tin Yu Wu, Wei Chen, Wei-Tsong Lee, Yong-Ping Huang
2012 J jnl
Wirel. Commun. Mob. Comput.
Tin Yu Wu, Yan-Bo Wang, Wei-Tsong Lee
2012 conf
AINA Workshops
Tzu-Ming Wang, Wei-Tsong Lee, Tin Yu Wu, Hsin-Wen Wei, Yu-San Lin
2011 conf
AINA Workshops
Yu-Hsuang Chen, Tin Yu Wu, Wei-Tsong Lee
2011 J jnl
J. Netw. Comput. Appl.
I-Ju Liao, Tin Yu Wu, Jen-Wen Ding, Wei-Tsong Lee
2011 conf
WCSP
Shih-Yeh Chen, Wei-Tsong Lee, Han-Chieh Chao, Yueh-Min Huang, Chin-Feng Lai
2011 conf
AINA Workshops
Ming-Yang Chen, Ya-Chun Li, Tin Yu Wu, Wei-Tsong Lee
2011 B conf
PIMRC
Tin Yu Wu, Wei-Tsong Lee, Fong-Hao Liu, Hung-Lin Chan, Tsung-Han Lin
2011 B conf
IWCMC
Guan-Lun Liao, Tin Yu Wu, Chi-Yuan Chen, Han-Chieh Chao, Wei-Tsong Lee
2011 J jnl
KSII Trans. Internet Inf. Syst.
Wei-Ting Cho, Chin-Feng Lai, Yueh-Min Huang, Wei-Tsong Lee, Sing-Wei Huangk
2011 J jnl
KSII Trans. Internet Inf. Syst.
Tin Yu Wu, Kai-Hua Kuo, Hua-Pu Cheng, Jen-Wen Ding, Wei-Tsong Lee
2010 C conf
GPC
I-Ju Liao, Hua-Pu Cheng, Tin Yu Wu, Wei-Tsong Lee
2010 conf
AINA Workshops
Yan-Bo Wang, Tin Yu Wu, Wei-Tsong Lee, Chih-Heng Ke
2010 conf
IIH-MSP
Chia-Feng Kang, Kai-Hua Kuo, Ya-Chun Li, Tin Yu Wu, Wei-Tsong Lee, Jim-Min Lin
2010 B conf
IWCMC
Tin Yu Wu, I-Ju Liao, Wei-Tsong Lee, Guan-Hsiung Liaw, Jen-Wen Ding, Chung-Chi Wu
2010 J jnl
J. Inf. Sci. Eng.
Tin Yu Wu, Ya-Chun Li, Wei-Tsong Lee, Jen-Wen Ding, Yu-Chin Wang
2009 conf
IIH-MSP
Po-Chang Huang, Kuo-Chih Chu, Hsiang-Fu Lo, Wei-Tsong Lee, Tin Yu Wu
2009 B conf
IWCMC
I-Ju Liao, Kuo-Chih Chu, Tin Yu Wu, Wei-Tsong Lee
2009 conf
ICIMCS
Hua-Pu Cheng, Tin-Yu Wu, Wei-Tsong Lee
2009 J jnl
IEEE Trans. Veh. Technol.
Shiann-Tsong Sheu, Yun-Yen Shih, Wei-Tsong Lee
2009 conf
HIS (1)
Kai-Yi Chin, Jim-Min Lin, Zeng-Wei Hong, Kun-Ta Lin, Wei-Tsong Lee
2009 conf
IIH-MSP
Huei-Yu Yang, Ya-Chun Li, Feng-Yi Chou, Tin Yu Wu, Wei-Tsong Lee
2009 conf
IIH-MSP
Hung-Wei Lin, Kuo-Chih Chu, Wei-Tsong Lee, Jim-Min Lin
2009 B conf
IWCMC
Ya-Chun Li, Tin Yu Wu, Wei-Tsong Lee, Yueh-Min Huang
2008 C conf
CIT
Wei-Tsong Lee, Fong-Hao Liu, Hsiang-Fu Lo
2008 conf
AINA Workshops
Jen-Chun Chiang, Hsiang-Fu Lo, Wei-Tsong Lee
2008 conf
ICOIN
Yu-Chin Wang, Hsiang-Fu Lo, Ya-Chun Li, Wei-Tsong Lee
2006 J jnl
IEEE Trans. Broadcast.
Wei-Tsong Lee, Kun-Chen Chung, Kuo-Chih Chu, Jen-Yi Pan
2006 J jnl
Adv. Eng. Informatics
Reu-Ching Chen, Wei-Tsong Lee, Jim-Min Lin
2005 J jnl
IEICE Trans. Commun.
Wei-Tsong Lee, Kuo-Chih Chu, Kun-Chen Chung, Jen-Yi Pan, Pau-Choo Chung
2004 J jnl
J. Inf. Sci. Eng.
Chi-Tai Lee, Jim-Min Lin, Zeng-Wei Hong, Wei-Tsong Lee
2004 J jnl
J. Inf. Sci. Eng.
Reu-Ching Chen, Wei-Tsong Lee, Jim-Min Lin
2003 B conf
WCNC
Jen-Yi Pan, Chin-Ping Tan, Wei-Tsong Lee
2003 J jnl
IEEE Trans. Veh. Technol.
Jen-Yi Pan, Wei-Tsong Lee, Nen-Fu Huang
2002 J jnl
Comput. Commun.
Kuo-Chih Chu, Yi-Gang Tai, Wei-Tsong Lee, Pau-Choo Chung
2002 conf
ICON
Jung-Tsan Lin, Wei-Tsong Lee
2002 conf
ICON
Chih Yuan Hung, Chin-Ping Tan, Li Chiung Chuang, Wei-Tsong Lee
2001 J jnl
Comput. Commun.
Jen-Yi Pan, Wei-Tsong Lee, Nen-Fu Huang
2001 B conf
GLOBECOM
Jen-Yi Pan, Wei-Tsong Lee, Nen-Fu Huang
2000 J jnl
IEEE Trans. Broadcast.
Fong-Hao Liu, C. C. Yang, Wei-Tsong Lee
1997 A* conf
INFOCOM
Jia-Wei Liao, Jun-Yao Wang, Wei-Tsong Lee, Ling-Yang Kung
1995 J jnl
IEEE/ACM Trans. Netw.
Wei-Tsong Lee, Ling-Yang Kung
1992 J jnl
Comput. Commun. Rev.
Wei-Tsong Lee, Ling-Yang Kung
1989 B conf
LCN
J. Y. Chao, Wei-Tsong Lee, Ling-Yang Kung
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