Oliver Holland

107 papers A* 1B 14Misc 1Journal 36Unranked 52
YearRankTypeTitle / Venue / Authors
2022 conf
VTC Spring
Mutasem Q. Hamdan, Chuan Heng Foh, Atta ul Quddus, Stephen Hancock, Oliver Holland, Richard Woodling
2021 J jnl
IEEE Trans. Cogn. Commun. Netw.
Chunxiao Jiang, Guoru Ding, Aly El Gamal, Andrea Zanella, Oliver Holland, Tim O'Shea
2020 J jnl
IEEE Instrum. Meas. Mag.
Gianni Cerro, Gianfranco Miele, Roger Hislop, Oliver Holland, Apurva N. Mody
2020 B conf
GLOBECOM
Adnan Aijaz, Zaher Dawy, Nikolaos Pappas, Meryem Simsek, Sharief Oteafy, Oliver Holland
2019 J jnl
IEEE Commun. Mag.
Bin Han, Vincenzo Sciancalepore, Oliver Holland, Mischa Dohler, Hans D. Schotten
2019 J jnl
Proc. IEEE
Eckehard G. Steinbach, Matti Strese, Mohamad A. Eid, Xun Liu, Amit Bhardwaj, Qian Liu, Mohammad Al Ja'afreh, Toktam Mahmoodi, Rania Hassen, Abdulmotaleb El Saddik, Oliver Holland
2019 J jnl
Proc. IEEE
Oliver Holland, Eckehard G. Steinbach, R. Venkatesha Prasad, Qian Liu, Zaher Dawy, Adnan Aijaz, Nikolaos Pappas, Kishor Chandra Joshi, Vijay S. Rao, Sharief Oteafy, Mohamad A. Eid, Mark A. Luden, Amit Bhardwaj, Xun Liu, Joachim Sachs, José Araújo
2018 conf
DySPAN
Oliver Holland, Barry Weaver, Andrew Mitchell, Michael Armitage, Steve Davison, Steve Gray, Andris Uplejs, Steve Jagger, Heikki Kokkinen
2018 J jnl
IEEE Commun. Mag.
Stanislav A. Filin, Dominique Noguet, Jean-Baptiste Dore, Baher Mawlawi, Oliver Holland, Muhammad Zeeshan Shakir, Hiroshi Harada, Fumihide Kojima
2018 J jnl
IEEE Commun. Mag.
Haitao Zhao, Nurul I. Sarkar, Jalel Ben-Othman, Oliver Holland, Amine Maaref, Wei Tan
2018 Misc conf
COMSNETS
Mohamed M. Kassem, Mahesh K. Marina, Oliver Holland
2018 J jnl
CoRR
Adnan Aijaz, Zaher Dawy, Nikolaos Pappas, Meryem Simsek, Sharief Oteafy, Oliver Holland
2017 conf
HAVE
Amit Bhardwaj, Burak Cizmeci, Eckehard G. Steinbach, Qian Liu, Mohamad A. Eid, José Araújo, Abdulmotaleb El Saddik, Ritu Kundu, Xun Liu, Oliver Holland, Mark A. Luden, Sharief Oteafy, R. Venkatesha Prasad
2017 J jnl
CoRR
Bin Han, Oliver Holland, Vincenzo Sciancalepore, Mischa Dohler, Hans D. Schotten
2017 J jnl
IEEE Trans. Veh. Technol.
Alexandros Palaios, Janne Riihijärvi, Oliver Holland, Petri Mähönen
2017 J jnl
IEEE Trans. Cogn. Commun. Netw.
Oliver Holland, Walid Saad
2017 J jnl
IEEE Commun. Mag.
Peter Rost, Christian Mannweiler, Diomidis S. Michalopoulos, Cinzia Sartori, Vincenzo Sciancalepore, Nishanth Sastry, Oliver Holland, Shreya Tayade, Bin Han, Dario Bega, Danish Aziz, Hajo Bakker
2017 J jnl
CoRR
Peter Rost, Christian Mannweiler, Diomidis S. Michalopoulos, Cinzia Sartori, Vincenzo Sciancalepore, Nishanth Sastry, Oliver Holland, Shreya Tayade, Bin Han, Dario Bega, Danish Aziz, Hajo Bakker
2017 conf
CSCN
Stan Wong, Nishanth Sastry, Oliver Holland, Vasilis Friderikos, Mischa Dohler, Hamid Aghvami
2016 conf
ICC
Oliver Holland, Adnan Aijaz, Shuyu Ping, Stan Wong, Jane Mack, Lisa Lam, Alejandro de la Fuente
2016 conf
VTC Spring
Oliver Holland, Aravindh Raman, Nishanth Sastry, Stan Wong, Jane Mack, Lisa Lam
2016 J jnl
IEEE Commun. Mag.
Oliver Holland, Adnan Aijaz, Florian Kaltenberger, Fotis Foukalas, Guillaume Vivier, Mateusz Buczkowski, Slawomir Pietrzyk
2016 conf
ICC Workshops
Azad Ravanshid, Peter Rost, Diomidis S. Michalopoulos, Vinh V. Phan, Hajo Bakker, Danish Aziz, Shreya Tayade, Hans D. Schotten, Stan Wong, Oliver Holland
2016 conf
VTC Spring
Oliver Holland
2016 J jnl
IEEE Internet Things J.
Yue Gao, Zhijin Qin, Zhiyong Feng, Qixun Zhang, Oliver Holland, Mischa Dohler
2016 J jnl
IEEE Trans. Cogn. Commun. Netw.
Oliver Holland
2016 conf
CSCN
Bernd Bochow, Oliver Holland, Konstantinos Katzis
2016 conf
VTC Fall
Qianyun Zhang, Xingjian Zhang, Oliver Holland, Mischa Dohler, Jean-Marc Chareau, Yue Gao, Pravir Chawdhry
2016 conf
ICT
Oliver Holland, Stan Wong, Vasilis Friderikos, Zhijin Qin, Yue Gao
2015 conf
MSAN
Assen Golaup, Oliver Holland, A. Hamid Aghvami
2015 J jnl
IEEE Trans. Cogn. Commun. Netw.
Adrian Kliks, Dionysia Triantafyllopoulou, Luca De Nardis, Oliver Holland, Liljana Gavrilovska, Aimilia Bantouna
2015 J jnl
Wirel. Pers. Commun.
Daniel Robalo, João R. Oliveira, Fernando J. Velez, Oliver Holland, A. Hamid Aghvami
2015 conf
GLOBECOM Workshops
Oliver Holland, Mischa Dohler
2015 conf
CSCN
Oliver Holland, Bernd Bochow, Konstantinos Katzis
2015 conf
CSCN
Stanislav A. Filin, Fumihide Kojima, Dominique Noguet, Jean-Baptiste Dore, Baher Mawlawi, Oliver Holland, Muhammad Zeeshan Shakir, Hiroshi Harada
2015 J jnl
CoRR
Adnan Aijaz, Nazir Uddin, Oliver Holland, A. Hamid Aghvami
2015 conf
DySPAN
Arturo Basaure, Oliver Holland
2015 J jnl
IEEE Trans. Commun.
Shuyu Ping, Adnan Aijaz, Oliver Holland, A. Hamid Aghvami
2015 conf
VTC Spring
Oliver Holland, Shuyu Ping, Nishanth Sastry, Pravir Chawdhry, Jean-Marc Chareau, James Bishop, Hong Xing, Suleyman Taskafa, Adnan Aijaz, Michele Bavaro, Philippe Viaud, Tiziano Pinato, Emanuele Angiuli, Mohammad Reza Akhavan, Julie A. McCann, Yue Gao, Zhijin Qin, Qianyun Zhang, Raymond Knopp, Florian Kaltenberger, Dominique Nussbaum, Rogerio Dionisio, José Carlos Ribeiro, Paulo Marques, Juhani Hallio, Mikko Jakobsson, Jani Auranen, Reijo Ekman, Heikki Kokkinen, Jarkko Paavola, Arto Kivinen, Tomaz Solc, Mihael Mohorcic, Ha Nguyen Tran, Kentaro Ishizu, Takeshi Matsumura, Kazuo Ibuka, Hiroshi Harada, Keiichi Mizutani
2015 conf
ICC Workshops
Shuyu Ping, Adnan Aijaz, Oliver Holland, A. Hamid Aghvami
2015 J jnl
IEEE Commun. Mag.
Adrian Kliks, Oliver Holland, Arturo Basaure, Marja Matinmikko
2015 conf
CSCN
Oliver Holland, Mischa Dohler
2015 J jnl
CoRR
Patrick Zwickl, Ivan Gojmerac, Paul Fuxjäger, Peter Reichl, Oliver Holland
2015 conf
DySPAN
Oliver Holland, Shuyu Ping, Adnan Aijaz, Jean-Marc Chareau, Pravir Chawdhry, Yue Gao, Zhijin Qin, Heikki Kokkinen
2015 ch.
Design and Deployment of Small Cell Networks
Adrian Kliks, Nikos Dimitriou, Andreas Zalonis, Oliver Holland
2014 A* conf
CCS
Oliver Holland, Nishanth Sastry, Shuyu Ping, Raymond Knopp, Florian Kaltenberger, Dominique Nussbaum, Juhani Hallio, Mikko Jakobsson, Jani Auranen, Reijo Ekman, Jarkko Paavola, Arto Kivinen, Ha Nguyen Tran, Kentaro Ishizu, Hiroshi Harada, Pravir Chawdhry, Jean-Marc Chareau, James Bishop, Michele Bavaro, Emanuele Angiuli, Yue Gao, Rogerio Dionisio, Paulo Marques, Heikki Kokkinen, Olli Luukkonen
2014 conf
WCNC Workshops
Nikos Dimitriou, Andreas Zalonis, Andreas Polydoros, Adrian Kliks, Oliver Holland
2014 conf
ICT
Jessica Acevedo Flores, Fernando J. Velez, Orlando Cabral, Daniel Robalo, Oliver Holland, A. Hamid Aghvami, Filippo Meucci, Albena Mihovska, Neeli R. Prasad, Ramjee Prasad
2014 B conf
WCNC
Shuyu Ping, Adnan Aijaz, Oliver Holland, A. Hamid Aghvami
2014 conf
DySPAN
Arturas Medeisis, John Sydor, Ligia Chira Cremene, Oliver Holland, Aurimas Anskaitis, Dariusz Wiecek, Yoram Haddad, Tomas Cuzanauskas
2014 conf
CrownCom
Wataru Yamada, Motoharu Sasaki, Takatoshi Sugiyama, Oliver Holland, Shuyu Ping, Bright Yeboah-Akowuah, Jiwon Hwang, Hamid Aghvami
2014 conf
CrownCom
Tomas Cuzanauskas, Arturas Medeisis, Yoram Haddad, Aurimas Anskaitis, Ligia Chira Cremene, John Sydor, Oliver Holland, Maziar M. Nekovee
2014 conf
CrownCom
Arturo Basaure, Oliver Holland
2014 conf
EuCNC
Florian Kaltenberger, Fotis Foukalas, Oliver Holland, Slawomir Pietrzyk, Somsai Thao, Guillaume Vivier
2013 B conf
PIMRC
Alexandros Palaios, Janne Riihijärvi, Oliver Holland, Petri Mähönen
2013 conf
EW
Ioannis Dagres, Andreas Polydoros, Daniel Denkovski, Valentin Rakovic, Vladimir Atanasovski, Liljana Gavrilovska, Krzysztof Cichon, Adrian Kliks, Shaswar Baban, Oliver Holland
2013 J jnl
Comput. Networks
Christian Facchini, Oliver Holland, Fabrizio Granelli, Nelson L. S. da Fonseca, Hamid Aghvami
2013 B conf
PIMRC
Shuyu Ping, Adnan Aijaz, Oliver Holland, A. Hamid Aghvami
2013 J jnl
IEEE Commun. Mag.
Hiroshi Harada, Yohannes D. Alemseged, Stanislav A. Filin, Maximilian Riegel, Michael Gundlach, Oliver Holland, Bernd Bochow, Masayuki Ariyoshi, Lynn Grande
2013 conf
ISWCS
Giuseppe Caso, Luca De Nardis, Oliver Holland, Maria-Gabriella Di Benedetto
2013 J jnl
IEEE Intell. Transp. Syst. Mag.
Hanna Bogucka, Oliver Holland
2013 conf
ICC
Auon Muhammad Akhtar, Luca De Nardis, Mohammad Reza Nakhai, Oliver Holland, Maria-Gabriella Di Benedetto, A. Hamid Aghvami
2013 conf
EW
Luca De Nardis, Maria-Gabriella Di Benedetto, Valentin Rakovic, Vladimir Atanasovski, Liljana Gavrilovska, Oliver Holland, Auon Muhammad Akhtar, Hamid Aghvami, Dimitri Tassetto, Sergio Bovelli, Vera Stavroulaki, Yiouli Kritikou, Aimilia Bantouna, Panagiotis Demestichas, Sylwia Romaszko
2013 B conf
PIMRC
Shaswar Baban, Daniel Denkovski, Oliver Holland, Liljana Gavrilovska, Hamid Aghvami
2013 conf
ICT
Adrian Kliks, Nikos Dimitriou, Andreas Zalonis, Oliver Holland
2013 conf
ISWCS
Shaswar Baban, Oliver Holland, Hamid Aghvami
2012 conf
SimuTools
Dimitri Tassetto, Sergio Bovelli, Auon Muhammad Akhtar, Oliver Holland, Luca De Nardis, Maria-Gabriella Di Benedetto
2012 J jnl
IEEE Wirel. Commun.
Raúl Chávez-Santiago, Keith E. Nolan, Oliver Holland, Luca De Nardis, João Miguel Ferro, Norberto Barroca, Luís M. Borges, Fernando J. Velez, Vânia Gonçalves, Ilangko Balasingham
2012 conf
CrownCom
Luca De Nardis, Maria-Gabriella Di Benedetto, Auon Muhammad Akhtar, Oliver Holland
2012 conf
GLOBECOM Workshops
Fernando J. Velez, João R. Oliveira, Daniel Robalo, Oliver Holland, A. Hamid Aghvami
2012 conf
CAMAD
Moshe T. Masonta, Yoram Haddad, Luca De Nardis, Adrian Kliks, Oliver Holland
2012 conf
ISWCS
Fernando J. Velez, João R. Oliveira, Daniel Robalo, Oliver Holland, A. Hamid Aghvami
2012 conf
ICC
Adnan Aijaz, Oliver Holland, Paul Pangalos, Hamid Aghvami
2012 conf
ISWCS
Luca De Nardis, Maria-Gabriella Di Benedetto, Dimitri Tassetto, Sergio Bovelli, Auon Muhammad Akhtar, Oliver Holland, Ragnar Thobaben
2012 J jnl
IEEE Commun. Mag.
Oliver Holland, A. Hamid Aghvami, Terence E. Dodgson, Hanna Bogucka
2012 conf
DySPAN
Alexandros Palaios, Janne Riihijärvi, Oliver Holland, Andreas Achtzehn, Petri Mähönen
2012 conf
DySPAN
Oliver Holland, Luca De Nardis, Keith E. Nolan, Arturas Medeisis, Peter Anker, Leo Fulvio Minervini, Fernando J. Velez, Marja Matinmikko, John Sydor
2012 conf
DySPAN
Arturas Medeisis, Oliver Holland, Luca De Nardis
2012 conf
WCNC Workshops
Oliver Holland, Hanna Bogucka, Maria-Gabriella Di Benedetto, Liljana Gavrilovska, Kamil Chudas
2011 J jnl
IEEE Commun. Surv. Tutorials
Gbenga Salami, Olasunkanmi Durowoju, Alireza Attar, Oliver Holland, Rahim Tafazolli, Hamid Aghvami
2011 conf
VTC Spring
Auon Muhammad Akhtar, Oliver Holland, Tuan Anh Le, Mohammad Reza Nakhai, A. Hamid Aghvami
2011 J jnl
IEEE Trans. Veh. Technol.
Fernando J. Velez, M. Kashif Nazir, A. Hamid Aghvami, Oliver Holland, Daniel Robalo
2011 conf
CrownCom
Hiroshi Harada, Yohannes D. Alemseged, Oliver Holland
2011 B conf
PIMRC
Oliver Holland, Orlando Cabral, Fernando J. Velez, Adnan Aijaz, Paul Pangalos, A. Hamid Aghvami
2011 conf
CogART
Terence E. Dodgson, Oliver Holland
2010 J jnl
IEEE Trans. Veh. Technol.
Joseph Mitola III, Alireza Attar, Honggang Zhang, Oliver Holland, Hiroshi Harada, Hamid Aghvami
2010 conf
ICC
Toktam Mahmoodi, Vasilis Friderikos, Oliver Holland, Hamid Aghvami
2010 J jnl
IET Commun.
Fernando J. Velez, A. Hamid Aghvami, Oliver Holland
2010 J jnl
IET Commun.
Toktam Mahmoodi, Vasilis Friderikos, Oliver Holland, A. Hamid Aghvami
2009 J jnl
IEEE Commun. Mag.
Soodesh Buljore, Hiroshi Harada, Stanislav A. Filin, Paul Houzé, Kostas Tsagkaris, Oliver Holland, Klaus Nolte, Tim Farnham, Vladimir Ivanov
2009 B conf
PIMRC
Alireza Attar, Oliver Holland, Mohammad Reza Nakhai, A. Hamid Aghvami
2009 conf
ICUMT
Timothy J. Harrold, Lingfeng (Stephen) Wang, Mark A. Beach, Gbenga Salami, Allahyar Yarmohammad, Oliver Holland
2008 B conf
PIMRC
Eleni Patouni, Oliver Holland, Nancy Alonistioti
2008 B conf
GLOBECOM
Toktam Mahmoodi, Vasilis Friderikos, Oliver Holland, Hamid Aghvami
2008 B conf
PIMRC
Toktam Mahmoodi, Oliver Holland, Vasilis Friderikos, A. Hamid Aghvami
2008 ch.
Encyclopedia of Wireless and Mobile Communications
Oliver Holland, Vasilis Friderikos, A. Hamid Aghvami
2008 conf
VTC Spring
Alireza Attar, Oliver Holland, Mohammad Reza Nakhai, A. Hamid Aghvami
2008 J jnl
IET Commun.
Alireza Attar, Oliver Holland, Mohammad Reza Nakhai, A. Hamid Aghvami
2008 J jnl
IEICE Trans. Commun.
Soodesh Buljore, Markus Muck, Patricia Martigne, Paul Houzé, Hiroshi Harada, Kentaro Ishizu, Oliver Holland, Andrej Mihailovic, Kostas Tsagkaris, Oriol Sallent, Gary Clemo, Mahesh Sooriyabandara, Vladimir Ivanov, Klaus Nolte, Makis Stametalos
2008 ch.
Encyclopedia of Wireless and Mobile Communications
Oliver Holland, Gbenga Salami, A. Hamid Aghvami
2007 conf
VTC Fall
Oliver Holland, Toktam Mahmoodi, A. Hamid Aghvami
2007 conf
VTC Fall
Toktam Mahmoodi, Vasilis Friderikos, Oliver Holland, A. Hamid Aghvami
2006 B conf
PIMRC
Oliver Holland, Alireza Attar, Nikolas Olaziregi, Nima Sattari, A. Hamid Aghvami
2006 J jnl
IEEE Trans. Mob. Comput.
Oliver Holland, A. Hamid Aghvami
2005 B conf
GLOBECOM
Oliver Holland, A. Hamid Aghvami
2005 B conf
PIMRC
Assen Golaup, Oliver Holland, A. Hamid Aghvami
2004 B conf
PIMRC
Oliver Holland, Qi Fan, A. Hamid Aghvami
tests/unit/test_apk_code_analyzer.py
← Index tests/unit/test_apk_code_analyzer.py python
"""
Unit tests for APK code analysis components.

Tests APKCodeAnalyzer, LibraryFilter, SmaliParser, method enumeration,
library filtering, and method key matching. All external tools are mocked.
"""
import os
import tempfile
import textwrap

import pytest
from unittest.mock import MagicMock, patch, PropertyMock

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


def _has_mmh3():
    try:
        import mmh3  # noqa: F401
        return True
    except ImportError:
        return False


# ============================================================================
# LibraryFilter Tests
# ============================================================================

class TestLibraryFilter:
    """Tests for package-based library filtering."""

    def test_default_prefixes_loaded(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        assert lf.is_library("android.app.Activity")
        assert lf.is_library("androidx.core.app.NotificationCompat")

    def test_user_class_not_filtered(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        assert not lf.is_library("com.example.myapp.MainActivity")
        assert not lf.is_library("org.myorg.MyClass")

    def test_dalvik_descriptor_format(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        assert lf.is_library("Landroid/app/Activity;")
        assert lf.is_library("Landroidx/core/app/NotificationCompat;")
        assert not lf.is_library("Lcom/example/myapp/MainActivity;")

    def test_custom_prefixes(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter(prefixes=["com.custom."])
        assert lf.is_library("com.custom.SomeClass")
        assert not lf.is_library("android.app.Activity")

    def test_env_var_override(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        with patch.dict(os.environ, {"APK_LIBRARY_PREFIXES": "my.lib.,other.lib."}):
            lf = LibraryFilter()
            assert lf.is_library("my.lib.SomeClass")
            assert lf.is_library("other.lib.AnotherClass")
            assert not lf.is_library("android.app.Activity")

    def test_filter_stats(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        lf.is_library("android.app.Activity")
        lf.is_library("com.example.MyClass")
        lf.is_library("kotlin.Unit")
        stats = lf.get_filter_stats()
        assert stats["library"] == 2
        assert stats["user"] == 1

    def test_normalize_class_name(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        assert LibraryFilter._normalize_class_name("Lcom/example/Foo;") == "com.example.Foo"
        assert LibraryFilter._normalize_class_name("com.example.Foo") == "com.example.Foo"
        assert LibraryFilter._normalize_class_name("com/example/Foo") == "com.example.Foo"

    def test_google_libraries_filtered(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        assert lf.is_library("com.google.android.gms.ads.AdView")
        assert lf.is_library("com.google.firebase.messaging.FirebaseMessagingService")
        assert lf.is_library("com.google.gson.Gson")

    def test_kotlin_filtered(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        assert lf.is_library("kotlin.Unit")
        assert lf.is_library("kotlinx.coroutines.CoroutineScope")

    def test_third_party_libs_filtered(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        assert lf.is_library("com.squareup.okhttp3.OkHttpClient")
        assert lf.is_library("io.reactivex.Observable")
        assert lf.is_library("org.apache.commons.io.IOUtils")

    def test_empty_class_name(self):
        from redb.extractors.decompiler.apk.library_filter import LibraryFilter
        lf = LibraryFilter()
        assert not lf.is_library("")


# ============================================================================
# SmaliParser Tests
# ============================================================================

class TestSmaliParser:
    """Tests for smali file parsing."""

    SAMPLE_SMALI = textwrap.dedent("""\
        .class public Lcom/example/MyClass;
        .super Ljava/lang/Object;

        .method public constructor <init>()V
            .registers 1
            invoke-direct {p0}, Ljava/lang/Object;-><init>()V
            return-void
        .end method

        .method public onCreate(Landroid/os/Bundle;)V
            .registers 4
            .param p1, "savedInstanceState"

            .line 10
            invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
            const/high16 v0, 0x7f090000
            invoke-virtual {p0, v0}, Lcom/example/MyClass;->setContentView(I)V
            const-string v1, "hello"
            invoke-virtual {p0, v1}, Lcom/example/MyClass;->log(Ljava/lang/String;)V
            return-void
        .end method

        .method public abstract doSomething()V
        .end method

        .method public native nativeMethod()V
        .end method
    """)

    def test_parse_smali_content(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content(self.SAMPLE_SMALI)
        # abstract and native should be skipped
        assert len(methods) == 2

    def test_method_names_extracted(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content(self.SAMPLE_SMALI)
        names = {m.method_name for m in methods}
        assert "<init>" in names
        assert "onCreate" in names

    def test_class_name_extracted(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content(self.SAMPLE_SMALI)
        for m in methods:
            assert m.class_name == "Lcom/example/MyClass;"

    def test_method_signature(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content(self.SAMPLE_SMALI)
        oncreate = [m for m in methods if m.method_name == "onCreate"][0]
        assert oncreate.method_signature == "(Landroid/os/Bundle;)V"

    def test_instruction_count(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content(self.SAMPLE_SMALI)
        oncreate = [m for m in methods if m.method_name == "onCreate"][0]
        # invoke-super, const/high16, invoke-virtual, const-string, invoke-virtual, return-void
        assert oncreate.instruction_count == 6

    def test_register_count(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content(self.SAMPLE_SMALI)
        oncreate = [m for m in methods if m.method_name == "onCreate"][0]
        assert oncreate.register_count == 4

    def test_register_count_locals_directive(self):
        """apktool outputs .locals by default, not .registers."""
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        smali_with_locals = textwrap.dedent("""\
            .class public Lcom/example/Locals;
            .super Ljava/lang/Object;

            .method public doWork()V
                .locals 3
                const/4 v0, 0x0
                const/4 v1, 0x1
                add-int v2, v0, v1
                return-void
            .end method
        """)
        methods = SmaliParser._parse_smali_content(smali_with_locals)
        assert len(methods) == 1
        assert methods[0].register_count == 3

    def test_abstract_native_skipped(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content(self.SAMPLE_SMALI)
        names = {m.method_name for m in methods}
        assert "doSomething" not in names
        assert "nativeMethod" not in names

    def test_normalize_smali_body(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        body = textwrap.dedent("""\
            .registers 4
            .line 10
            # comment
            invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

            const-string v1, "hello"
        """)
        normalized = SmaliParser.normalize_smali_body(body)
        assert ".line" not in normalized
        assert "# comment" not in normalized
        assert "invoke-super" in normalized
        assert "const-string" in normalized

    def test_count_instructions(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        body = textwrap.dedent("""\
            .registers 4
            .param p1, "x"
            invoke-virtual {p0}, Lcom/example/Foo;->bar()V
            const/4 v0, 0x0
            :label_0
            if-eqz v0, :label_1
            return-void
            :label_1
            goto :label_0
        """)
        assert SmaliParser.count_instructions(body) == 5

    def test_make_method_key(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        key = SmaliParser.make_method_key(
            "Lcom/example/Foo;", "bar", "(I)V"
        )
        assert key == "Lcom/example/Foo;->bar(I)V"

    def test_parse_smali_file(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".smali", delete=False
        ) as f:
            f.write(self.SAMPLE_SMALI)
            f.flush()
            try:
                methods = SmaliParser.parse_smali_file(f.name)
                assert len(methods) == 2
            finally:
                os.unlink(f.name)

    def test_parse_smali_directory(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        with tempfile.TemporaryDirectory() as tmpdir:
            # Create nested dir structure
            pkg_dir = os.path.join(tmpdir, "com", "example")
            os.makedirs(pkg_dir)
            with open(os.path.join(pkg_dir, "MyClass.smali"), "w") as f:
                f.write(self.SAMPLE_SMALI)

            result = SmaliParser.parse_smali_directory(tmpdir)
            assert len(result) == 2
            # Check keys are in expected format
            for key in result:
                assert "->" in key

    def test_empty_smali_file(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        methods = SmaliParser._parse_smali_content("")
        assert methods == []

    def test_class_only_no_methods(self):
        from redb.extractors.decompiler.apk.smali_parser import SmaliParser
        content = ".class public Lcom/example/Empty;\n.super Ljava/lang/Object;\n"
        methods = SmaliParser._parse_smali_content(content)
        assert methods == []


# ============================================================================
# MethodExtractor Tests (Hashing, Obfuscation, Type Conversion)
# ============================================================================

class TestMethodExtractor:
    """Tests for method-level content extraction and hashing."""

    def test_compute_sha256(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_sha256
        h = compute_sha256("test content")
        assert len(h) == 64
        assert h == compute_sha256("test content")  # deterministic

    def test_compute_sha256_different_input(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_sha256
        assert compute_sha256("a") != compute_sha256("b")

    def test_compute_ssdeep_short_input(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_ssdeep
        result = compute_ssdeep("short")
        assert result is None

    def test_compute_ssdeep_long_input(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_ssdeep
        result = compute_ssdeep("x" * 200)
        # May be None depending on entropy, but shouldn't crash
        assert result is None or isinstance(result, str)

    def test_compute_tlsh_short_input(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_tlsh
        result = compute_tlsh("short")
        assert result is None

    def test_compute_tlsh_long_input(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_tlsh
        data = "".join(chr(i % 256) for i in range(200))
        result = compute_tlsh(data)
        assert result is None or isinstance(result, str)

    def test_compute_minhash_returns_none_without_mmh3(self):
        from unittest.mock import patch
        from redb.extractors.decompiler.apk.method_extractor import compute_minhash
        with patch.dict("sys.modules", {"mmh3": None}):
            # Force reimport to hit ImportError
            import importlib
            import redb.extractors.decompiler.apk.method_extractor as mod
            importlib.reload(mod)
            result = mod.compute_minhash("line1\nline2\nline3\nline4")
            # mmh3 may or may not be available; just check it doesn't crash
            assert result is None or isinstance(result, list)
            importlib.reload(mod)  # restore

    def test_compute_minhash_too_few_lines(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_minhash
        result = compute_minhash("invoke-direct {v0}, Lfoo;->bar()V")
        # Only 1 instruction line, need at least 3 for 3-grams
        assert result is None

    @pytest.mark.skipif(
        not _has_mmh3(), reason="mmh3 not installed"
    )
    def test_compute_minhash_deterministic(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_minhash
        smali = (
            "invoke-direct {v1}, Ljava/lang/Object;-><init>()V\n"
            "const-string v0, \"hello\"\n"
            "iput-object v0, v1, LA;->a:Ljava/lang/String;\n"
            "return-void"
        )
        sig1 = compute_minhash(smali)
        sig2 = compute_minhash(smali)
        assert sig1 is not None
        assert sig1 == sig2

    @pytest.mark.skipif(
        not _has_mmh3(), reason="mmh3 not installed"
    )
    def test_compute_minhash_signature_length(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_minhash
        smali = (
            "invoke-direct {v1}, Ljava/lang/Object;-><init>()V\n"
            "const-string v0, \"hello\"\n"
            "iput-object v0, v1, LA;->a:Ljava/lang/String;\n"
            "return-void"
        )
        sig = compute_minhash(smali)
        assert sig is not None
        assert len(sig) == 64  # SIGNATURE_LENGTH

    @pytest.mark.skipif(
        not _has_mmh3(), reason="mmh3 not installed"
    )
    def test_compute_minhash_value_range(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_minhash
        smali = (
            "invoke-direct {v1}, Ljava/lang/Object;-><init>()V\n"
            "const-string v0, \"hello\"\n"
            "iput-object v0, v1, LA;->a:Ljava/lang/String;\n"
            "return-void"
        )
        sig = compute_minhash(smali)
        assert sig is not None
        for val in sig:
            assert 0 <= val < 256  # 8-bit values

    @pytest.mark.skipif(
        not _has_mmh3(), reason="mmh3 not installed"
    )
    def test_compute_minhash_different_inputs_differ(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_minhash
        smali_a = (
            "invoke-direct {v1}, Ljava/lang/Object;-><init>()V\n"
            "const-string v0, \"hello\"\n"
            "iput-object v0, v1, LA;->a:Ljava/lang/String;\n"
            "return-void"
        )
        smali_b = (
            "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;\n"
            "const-string v1, \"world\"\n"
            "invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V\n"
            "return-void"
        )
        sig_a = compute_minhash(smali_a)
        sig_b = compute_minhash(smali_b)
        assert sig_a is not None
        assert sig_b is not None
        assert sig_a != sig_b

    @pytest.mark.skipif(
        not _has_mmh3(), reason="mmh3 not installed"
    )
    def test_compute_minhash_skips_directives_and_labels(self):
        from redb.extractors.decompiler.apk.method_extractor import compute_minhash
        # Directives and labels should be filtered out
        smali = (
            ".registers 3\n"
            ":start\n"
            "invoke-direct {v1}, Ljava/lang/Object;-><init>()V\n"
            ".line 10\n"
            "const-string v0, \"hello\"\n"
            "iput-object v0, v1, LA;->a:Ljava/lang/String;\n"
            "return-void"
        )
        smali_clean = (
            "invoke-direct {v1}, Ljava/lang/Object;-><init>()V\n"
            "const-string v0, \"hello\"\n"
            "iput-object v0, v1, LA;->a:Ljava/lang/String;\n"
            "return-void"
        )
        sig_with_directives = compute_minhash(smali)
        sig_clean = compute_minhash(smali_clean)
        assert sig_with_directives == sig_clean

    def test_dalvik_to_java_class(self):
        from redb.extractors.decompiler.apk.method_extractor import dalvik_to_java_class
        assert dalvik_to_java_class("Lcom/example/Foo;") == "com.example.Foo"
        assert dalvik_to_java_class("com.example.Foo") == "com.example.Foo"

    def test_dalvik_type_to_java(self):
        from redb.extractors.decompiler.apk.method_extractor import dalvik_type_to_java
        assert dalvik_type_to_java("V") == "void"
        assert dalvik_type_to_java("I") == "int"
        assert dalvik_type_to_java("Z") == "boolean"
        assert dalvik_type_to_java("Ljava/lang/String;") == "String"
        assert dalvik_type_to_java("[I") == "int[]"
        assert dalvik_type_to_java("[Ljava/lang/String;") == "String[]"

    def test_dalvik_to_java_prototype(self):
        from redb.extractors.decompiler.apk.method_extractor import dalvik_to_java_prototype
        result = dalvik_to_java_prototype("onCreate", "(Landroid/os/Bundle;)V")
        assert result == "void onCreate(Bundle)"

    def test_dalvik_to_java_prototype_no_params(self):
        from redb.extractors.decompiler.apk.method_extractor import dalvik_to_java_prototype
        result = dalvik_to_java_prototype("toString", "()Ljava/lang/String;")
        assert result == "String toString()"

    def test_dalvik_to_java_prototype_multiple_params(self):
        from redb.extractors.decompiler.apk.method_extractor import dalvik_to_java_prototype
        result = dalvik_to_java_prototype("foo", "(ILjava/lang/String;Z)V")
        assert result == "void foo(int, String, boolean)"

    def test_detect_obfuscation_short_name(self):
        from redb.extractors.decompiler.apk.method_extractor import detect_obfuscation_indicators
        result = detect_obfuscation_indicators("a", "Lcom/example/Foo;", "", 10)
        assert result["short_method_name"] is True
        result = detect_obfuscation_indicators("onCreate", "Lcom/example/Foo;", "", 10)
        assert result["short_method_name"] is False

    def test_detect_obfuscation_short_class(self):
        from redb.extractors.decompiler.apk.method_extractor import detect_obfuscation_indicators
        result = detect_obfuscation_indicators("foo", "Lcom/example/a;", "", 10)
        assert result["short_class_name"] is True
        result = detect_obfuscation_indicators("foo", "Lcom/example/MainActivity;", "", 10)
        assert result["short_class_name"] is False

    def test_detect_reflection_calls(self):
        from redb.extractors.decompiler.apk.method_extractor import detect_obfuscation_indicators
        smali = "invoke-virtual {v0}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"
        result = detect_obfuscation_indicators("foo", "Lcom/Foo;", smali, 10)
        assert result["has_reflection_calls"] is True

    def test_detect_no_reflection(self):
        from redb.extractors.decompiler.apk.method_extractor import detect_obfuscation_indicators
        smali = "invoke-virtual {p0}, Lcom/example/Foo;->bar()V"
        result = detect_obfuscation_indicators("foo", "Lcom/Foo;", smali, 10)
        assert result["has_reflection_calls"] is False

    def test_detect_string_encryption(self):
        from redb.extractors.decompiler.apk.method_extractor import detect_obfuscation_indicators
        smali = 'const-string v0, "encrypted"\n    invoke-static {v0}, Lcom/example/Crypto;->decrypt(Ljava/lang/String;)Ljava/lang/String;'
        result = detect_obfuscation_indicators("foo", "Lcom/Foo;", smali, 10)
        assert result["has_string_encryption"] is True

    def test_detect_excessive_goto(self):
        from redb.extractors.decompiler.apk.method_extractor import detect_obfuscation_indicators
        smali = "\n".join(["goto :label_0"] * 20)
        result = detect_obfuscation_indicators("foo", "Lcom/Foo;", smali, 30)
        # threshold = max(5, 30*0.15) = 5, 20 > 5
        assert result["excessive_goto_count"] is True

    def test_detect_no_excessive_goto(self):
        from redb.extractors.decompiler.apk.method_extractor import detect_obfuscation_indicators
        smali = "goto :label_0\nreturn-void"
        result = detect_obfuscation_indicators("foo", "Lcom/Foo;", smali, 100)
        assert result["excessive_goto_count"] is False

    def test_parse_dalvik_params_empty(self):
        from redb.extractors.decompiler.apk.method_extractor import _parse_dalvik_params
        assert _parse_dalvik_params("") == []

    def test_parse_dalvik_params_primitives(self):
        from redb.extractors.decompiler.apk.method_extractor import _parse_dalvik_params
        result = _parse_dalvik_params("IZJ")
        assert result == ["I", "Z", "J"]

    def test_parse_dalvik_params_object(self):
        from redb.extractors.decompiler.apk.method_extractor import _parse_dalvik_params
        result = _parse_dalvik_params("Ljava/lang/String;")
        assert result == ["Ljava/lang/String;"]

    def test_parse_dalvik_params_mixed(self):
        from redb.extractors.decompiler.apk.method_extractor import _parse_dalvik_params
        result = _parse_dalvik_params("ILjava/lang/String;Z")
        assert result == ["I", "Ljava/lang/String;", "Z"]

    def test_parse_dalvik_params_array(self):
        from redb.extractors.decompiler.apk.method_extractor import _parse_dalvik_params
        result = _parse_dalvik_params("[I[Ljava/lang/String;")
        assert result == ["[I", "[Ljava/lang/String;"]


# ============================================================================
# APKCodeAnalyzer Tests (mocked tools)
# ============================================================================

class TestAPKCodeAnalyzer:
    """Tests for the APKCodeAnalyzer orchestration."""

    def test_init_defaults(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        analyzer = APKCodeAnalyzer("/fake/path.apk")
        assert analyzer.filepath == "/fake/path.apk"
        assert analyzer.timeout == 600
        assert analyzer.min_instructions == 5

    def test_init_custom_timeout(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        analyzer = APKCodeAnalyzer("/fake/path.apk", timeout=300)
        assert analyzer.timeout == 300

    def test_init_min_instructions_env(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        with patch.dict(os.environ, {"APK_MIN_METHOD_INSTRUCTIONS": "10"}):
            analyzer = APKCodeAnalyzer("/fake/path.apk")
            assert analyzer.min_instructions == 10

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    @patch.object(
        __import__("redb.extractors.decompiler.apk.apktool_wrapper", fromlist=["ApktoolDisassembler"]).ApktoolDisassembler,
        "disassemble",
        return_value=False,
    )
    @patch.object(
        __import__("redb.extractors.decompiler.apk.jadx_wrapper", fromlist=["JADXDecompiler"]).JADXDecompiler,
        "decompile",
        return_value=False,
    )
    def test_extract_returns_dict_keys(self, mock_jadx, mock_apktool, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        mock_analysis = MagicMock()
        mock_analysis.get_methods.return_value = []
        mock_androguard.return_value = (MagicMock(), [], mock_analysis)

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        assert "decompiled_content" in results
        assert "decompiled_refs" in results
        assert "smali_content" in results
        assert "smali_refs" in results
        assert "similarity_metrics" in results
        assert "strings" in results
        assert "analysis_errors" in results
        analyzer.cleanup()

    @patch("redb.extractors.decompiler.apk.analyzer.APKCodeAnalyzer._run_androguard")
    def test_extract_androguard_failure(self, mock_androguard):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        mock_androguard.side_effect = Exception("Analysis failed")

        analyzer = APKCodeAnalyzer("/fake/path.apk", log=MagicMock())
        results = analyzer.extract()

        assert len(results["analysis_errors"]) > 0
        analyzer.cleanup()

    def test_cleanup_removes_temp_dirs(self):
        from redb.extractors.decompiler.apk.analyzer import APKCodeAnalyzer
        analyzer = APKCodeAnalyzer("/fake/path.apk")
        tmpdir = tempfile.mkdtemp()
        analyzer._temp_dirs.append(tmpdir)
        assert os.path.isdir(tmpdir)
        analyzer.cleanup()
        assert not os.path.isdir(tmpdir)

    def test_normalize_java(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_java
        source = "  public void foo() {\n    int x = 1;\n  }\n"
        normalized = _normalize_java(source)
        assert normalized == "public void foo() {\nint x = 1;\n}"


class TestNormalizeAndroguardOperands:
    """Tests for androguard-to-apktool output normalization."""

    def test_invoke_adds_braces(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "invoke-direct",
            "v1, Ljava/lang/Object;-><init>()V"
        )
        assert result == "{v1}, Ljava/lang/Object;-><init>()V"

    def test_invoke_virtual_multiple_regs(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "invoke-virtual",
            "v0, v1, Ljava/lang/String;->equals(Ljava/lang/Object;)Z"
        )
        assert result == "{v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z"

    def test_invoke_static_no_regs(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        # invoke-static with no register args (rare but possible)
        result = _normalize_androguard_operands(
            "invoke-static",
            "Ljava/lang/System;->gc()V"
        )
        # First part starts with L, so no registers to wrap
        assert result == "Ljava/lang/System;->gc()V"

    def test_invoke_range(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "invoke-virtual/range",
            "v3, v4, v5, Landroid/util/Log;->d(Ljava/lang/String; Ljava/lang/String;)I"
        )
        assert result == "{v3, v4, v5}, Landroid/util/Log;->d(Ljava/lang/String; Ljava/lang/String;)I"

    def test_iget_field_colon(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "iget-object",
            "v0, v4, LF;->a Ljava/lang/String;"
        )
        assert result == "v0, v4, LF;->a:Ljava/lang/String;"

    def test_iput_field_colon(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "iput-object",
            "v0, v1, LA;->b Ljava/lang/String;"
        )
        assert result == "v0, v1, LA;->b:Ljava/lang/String;"

    def test_sget_primitive_field(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "sget",
            "v0, Lcom/Foo;->count I"
        )
        assert result == "v0, Lcom/Foo;->count:I"

    def test_sput_array_field(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "sput-object",
            "v0, Lcom/Foo;->data [B"
        )
        assert result == "v0, Lcom/Foo;->data:[B"

    def test_non_invoke_non_field_passthrough(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands(
            "const-string",
            'v0, "hello world"'
        )
        assert result == 'v0, "hello world"'

    def test_move_passthrough(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands("move", "v0, v4")
        assert result == "v0, v4"

    def test_goto_passthrough(self):
        from redb.extractors.decompiler.apk.analyzer import _normalize_androguard_operands
        result = _normalize_androguard_operands("goto", "+005h")
        assert result == "+005h"


class TestSmaliCFG:
    """Tests for smali CFG construction and metrics computation."""

    def test_empty_body(self):
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        m = compute_cfg_metrics("")
        assert m.block_count == 0
        assert m.cyclomatic_complexity == 1

    def test_linear_method_apktool(self):
        """Single basic block — no branches."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 2
            invoke-direct {v1}, Ljava/lang/Object;-><init>()V
            const-string v0, "hello"
            iput-object v0, v1, LA;->a:Ljava/lang/String;
            return-void
        """)
        m = compute_cfg_metrics(smali)
        assert m.block_count == 1
        assert m.edge_count == 0
        assert m.cyclomatic_complexity == 1
        assert m.loop_count == 0
        assert m.max_depth == 0
        assert m.max_fan_out == 0

    def test_linear_method_androguard(self):
        """Single basic block — androguard format (no labels/directives)."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = (
            "    invoke-direct {v1}, Ljava/lang/Object;-><init>()V\n"
            "    const-string v0, \"hello\"\n"
            "    iput-object v0, v1, LA;->a:Ljava/lang/String;\n"
            "    return-void"
        )
        m = compute_cfg_metrics(smali)
        assert m.block_count == 1
        assert m.edge_count == 0
        assert m.cyclomatic_complexity == 1

    def test_if_branch_apktool(self):
        """Diamond pattern: if-else with two paths merging."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 2
            const/4 v0, 0x0
            if-eqz v0, :cond_0
            const/4 v0, 0x1
            goto :goto_0
            :cond_0
            const/4 v0, 0x2
            :goto_0
            return v0
        """)
        m = compute_cfg_metrics(smali)
        # Blocks: [const,if] [const,goto] [const] [return]
        assert m.block_count == 4
        assert m.cyclomatic_complexity == 2  # E - N + 2
        assert m.loop_count == 0
        assert m.max_fan_out == 2  # if-branch has 2 successors

    def test_loop_apktool(self):
        """Simple loop: goto back to earlier label."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 2
            const/4 v0, 0x0
            :loop_start
            add-int/lit8 v0, v0, 0x1
            const/16 v1, 0xa
            if-lt v0, v1, :loop_start
            return v0
        """)
        m = compute_cfg_metrics(smali)
        assert m.loop_count == 1  # back edge from if-lt to loop_start
        assert m.cyclomatic_complexity >= 2

    def test_if_branch_androguard(self):
        """If-else in androguard offset format."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = (
            "    const/4 v0, 0x0\n"
            "    if-eqz v0, +2h\n"        # skip 2 instructions ahead
            "    const/4 v0, 0x1\n"
            "    goto +1h\n"               # skip 1 ahead
            "    const/4 v0, 0x2\n"
            "    return v0"
        )
        m = compute_cfg_metrics(smali)
        assert m.block_count >= 3
        assert m.cyclomatic_complexity >= 2

    def test_multiple_if_branches(self):
        """Multiple if-branches increase cyclomatic complexity."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 3
            if-eqz v0, :cond_0
            if-eqz v1, :cond_1
            const/4 v2, 0x0
            goto :end
            :cond_0
            const/4 v2, 0x1
            goto :end
            :cond_1
            const/4 v2, 0x2
            :end
            return v2
        """)
        m = compute_cfg_metrics(smali)
        # Two if-branches: CC should be >= 3
        assert m.cyclomatic_complexity >= 3

    def test_return_terminates_block(self):
        """Return instruction terminates block with no successor."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 1
            const/4 v0, 0x0
            return v0
        """)
        m = compute_cfg_metrics(smali)
        assert m.block_count == 1
        assert m.edge_count == 0

    def test_throw_terminates_block(self):
        """Throw instruction terminates block with no successor."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 1
            new-instance v0, Ljava/lang/RuntimeException;
            invoke-direct {v0}, Ljava/lang/RuntimeException;-><init>()V
            throw v0
        """)
        m = compute_cfg_metrics(smali)
        assert m.block_count == 1
        assert m.edge_count == 0

    def test_max_depth_linear_chain(self):
        """Chain of blocks gives increasing depth."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        # 3 blocks chained: entry -> if -> fall-through -> return
        smali = textwrap.dedent("""\
            .registers 2
            const/4 v0, 0x0
            if-nez v0, :cond_0
            const/4 v0, 0x1
            :cond_0
            return v0
        """)
        m = compute_cfg_metrics(smali)
        assert m.max_depth >= 1

    def test_nested_loop_apktool(self):
        """Nested loop: two back edges."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 3
            const/4 v0, 0x0
            :outer
            const/4 v1, 0x0
            :inner
            add-int/lit8 v1, v1, 0x1
            if-lt v1, v2, :inner
            add-int/lit8 v0, v0, 0x1
            if-lt v0, v2, :outer
            return-void
        """)
        m = compute_cfg_metrics(smali)
        assert m.loop_count == 2

    def test_consistency_formula(self):
        """Verify E - N + 2 formula matches cyclomatic_complexity."""
        from redb.extractors.decompiler.apk.smali_cfg import compute_cfg_metrics
        smali = textwrap.dedent("""\
            .registers 2
            const/4 v0, 0x0
            if-eqz v0, :cond_0
            const/4 v0, 0x1
            goto :goto_0
            :cond_0
            const/4 v0, 0x2
            :goto_0
            return v0
        """)
        m = compute_cfg_metrics(smali)
        expected_cc = m.edge_count - m.block_count + 2
        assert m.cyclomatic_complexity == max(1, expected_cc)