Jaan Praks

95 papers C 36Journal 54Unranked 5
YearRankTypeTitle / Venue / Authors
2025 J jnl
IEEE Access
Mayank, Fabricio dos Santos Prol, Ville Lundén, Zainab Saleem, Guillem Foreman-Campins, Shikha Sharma, Elena Simona Lohan, Mohammad Zahidul H. Bhuiyan, Sanna Kaasalainen, Heidi Kuusniemi, Jaan Praks
2025 J jnl
Int. J. Satell. Commun. Netw.
Kaan Çelikbilek, Elena Simona Lohan, Jaan Praks
2024 J jnl
IEEE Trans. Geosci. Remote. Sens.
Jorge Jorge Ruiz, Ioanna Merkouriadi, Juha Lemmetyinen, Juval Cohen, Anna Kontu, Thomas Nagler, Jouni Pulliainen, Jaan Praks
2024 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Ghasem Ronoud, Ali Asghar Darvishsefat, Maryam Poorazimy, Erkki Tomppo, Oleg Antropov, Jaan Praks
2024 conf
WiSEE
Mayank, Fabricio dos Santos Prol, Ville Lundén, Elena Simona Lohan, Zainab Saleem, Shikha Sharma, Mohammad Zahidul H. Bhuiyan, Sanna Kaasalainen, Heidi Kuusniemi, Jaan Praks
2024 J jnl
IEEE Trans. Aerosp. Electron. Syst.
Fabricio dos Santos Prol, Mohammad Zahidul H. Bhuiyan, Sanna Kaasalainen, Elena Simona Lohan, Jaan Praks, Kaan Çelikbilek, Heidi Kuusniemi
2024 J jnl
Remote. Sens.
Katriina Veijola, Juval Cohen, Marko Mäkynen, Juha Lemmetyinen, Jaan Praks, Bin Cheng
2023 J jnl
Remote. Sens.
Maryam Poorazimy, Shaban Shataee, Hossein Aghababaei, Erkki Tomppo, Jaan Praks
2023 J jnl
EURASIP J. Adv. Signal Process.
Christina Pinell, Fabricio dos Santos Prol, Mohammad Zahidul H. Bhuiyan, Jaan Praks
2023 J jnl
Remote. Sens.
Shaojia Ge, Erkki Tomppo, Yrjö Rauste, Ronald E. McRoberts, Jaan Praks, Hong Gu, Weimin Su, Oleg Antropov
2023 conf
PLANS
Fabricio dos Santos Prol, Sanna Kaasalainen, Elena Simona Lohan, Mohammad Zahidul H. Bhuiyan, Jaan Praks, Heidi Kuusniemi
2022 J jnl
IEEE Geosci. Remote. Sens. Lett.
Juha Lemmetyinen, Jorge Jorge Ruiz, Juval Cohen, Jouko Haapamaa, Anna Kontu, Jouni Pulliainen, Jaan Praks
2022 C conf
IGARSS
Tauri Tampuu, Francesco De Zan, Robert Shau, Jaan Praks, Marko Kohv, Ain Kull
2022 C conf
IGARSS
Shaojia Ge, Hong Gu, Weimin Su, Jaan Praks, Anne Lönnqvist, Oleg Antropov
2022 J jnl
Remote. Sens.
Shaojia Ge, Weimin Su, Hong Gu, Yrjö Rauste, Jaan Praks, Oleg Antropov
2022 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Shaojia Ge, Hong Gu, Weimin Su, Jaan Praks, Oleg Antropov
2022 J jnl
IEEE Trans. Geosci. Remote. Sens.
Jorge Jorge Ruiz, Juha Lemmetyinen, Anna Kontu, Riku Tarvainen, Risto Vehmas, Jouni Pulliainen, Jaan Praks
2022 J jnl
IEEE Commun. Mag.
Ruben Morales Ferre, Elena Simona Lohan, Heidi Kuusniemi, Jaan Praks, Sanna Kaasalainen, Christina Pinell, Mahmoud Elsanhoury
2022 J jnl
IEEE Trans. Aerosp. Electron. Syst.
Bagus Adiwiluhung Riwanto, Petri Niemela, Hendrik Ehrpais, Andris Slavinskis, Muhammad Rizwan Mughal, Jaan Praks
2022 J jnl
IEEE Access
Fabricio dos Santos Prol, Ruben Morales Ferre, Zainab Saleem, Petri Välisuo, Christina Pinell, Elena Simona Lohan, Mahmoud Elsanhoury, Mohammed S. Elmusrati, Saiful Islam, Kaan Çelikbilek, Kannan Selvan, Jaakko Yliaho, K. Rutledge, Arto Ojala, Luca Ferranti, Jaan Praks, Mohammad Zahidul H. Bhuiyan, Sanna Kaasalainen, Heidi Kuusniemi
2022 J jnl
Sensors
Kaan Çelikbilek, Zainab Saleem, Ruben Morales Ferre, Jaan Praks, Elena Simona Lohan
2021 C conf
IGARSS
Jorge Jorge Ruiz, Juha Lemmetyinen, Anna Kontu, Riku Tarvainen, Jouni Pulliainen, Risto Vehmas, Jaan Praks
2021 J jnl
Int. J. Appl. Earth Obs. Geoinformation
Tauri Tampuu, Jaan Praks, Ain Kull, Rivo Uiboupin, Tanel Tamm, Kaupo Voormansik
2021 J jnl
Remote. Sens.
Erkki Tomppo, Ghasem Ronoud, Oleg Antropov, Harri Hytönen, Jaan Praks
2021 J jnl
Remote. Sens.
Erkki Tomppo, Guangxing Wang, Jaan Praks, Ronald E. McRoberts, Lars T. Waser
2021 J jnl
Remote. Sens.
Oleg Antropov, Yrjö Rauste, Jaan Praks, Frank Martin Seifert, Tuomas Häme
2021 J jnl
Geo spatial Inf. Sci.
Marjan Marbouti, Oleg Antropov, Jaan Praks, Patrick Eriksson, Vahid Arabzadeh, Eero Rinne, Matti Leppäranta
2021 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Sanja Scepanovic, Oleg Antropov, Pekka Laurila, Yrjö Rauste, Vladimir Ignatenko, Jaan Praks
2020 C conf
IGARSS
Oleg Antropov, Yrjö Rauste, Sanja Scepanovic, Vladimir Ignatenko, Anne Lönnqvist, Jaan Praks
2020 C conf
IGARSS
Tauri Tampuu, Jaan Praks, Ain Kull
2020 J jnl
Remote. Sens.
Tauri Tampuu, Jaan Praks, Rivo Uiboupin, Ain Kull
2020 J jnl
IEEE Trans. Aerosp. Electron. Syst.
Muhammad Rizwan Mughal, Hassan Ali, Anwar Ali, Jaan Praks, Leonardo M. Reyneri
2020 C conf
IGARSS
Shaojia Ge, Erkki Tomppo, Yrjö Rauste, Weimin Su, Hong Gu, Jaan Praks, Oleg Antropov
2020 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Alexander W. Jacob, Claudia Notarnicola, Gopika Suresh, Oleg Antropov, Shaojia Ge, Jaan Praks, Yifang Ban, Eric Pottier, Jordi J. Mallorquí, Javier Duro, Marcus E. Engdahl, Fernando Vicente-Guijalba, Carlos López-Martínez, Juan M. Lopez-Sanchez, Marius Litzinger, Harald Kristen, Alejandro Mestre-Quereda, Dariusz Ziolkowski, Marco Lavalle
2020 J jnl
Remote. Sens.
Kaupo Voormansik, Karlis Zalite, Indrek Sünter, Tanel Tamm, Kalev Koppel, Tarvi Verro, Agris Brauns, Dainis Jakovels, Jaan Praks
2020 J jnl
Sensors
Jorge Jorge Ruiz, Risto Vehmas, Juha Lemmetyinen, Josu Uusitalo, Janne Lahtinen, Kari Lehtinen, Anna Kontu, Kimmo Rautiainen, Riku Tarvainen, Jouni Pulliainen, Jaan Praks
2019 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Juha Kainulainen, Sampo Salo, Janne Lahtinen, Guifré Molera Calvés, Jaakko Seppänen, Jaan Praks, Teemu Hakala, Yuwei Chen, Juha Hyyppä, Martin Unwin, Philip Jales, Gerhard Ressler, Tânia Casal, Josep Roselló
2019 J jnl
Remote. Sens.
Erkki Tomppo, Oleg Antropov, Jaan Praks
2019 J jnl
Remote. Sens.
Erkki Tomppo, Oleg Antropov, Jaan Praks
2019 C conf
IGARSS
Shaojia Ge, Oleg Antropov, Weimin Su, Hong Gu, Jaan Praks
2019 J jnl
Int. J. Satell. Commun. Netw.
Muhammad Rizwan Mughal, Anwar Ali, Jaan Praks, Leonardo M. Reyneri
2019 J jnl
Remote. Sens.
Fran Fabra, Estel Cardellach, Serni Ribó, Weiqiang Li, Antonio Rius, Juan Carlos Arco-Fernández, Oleguer Nogués-Correig, Jaan Praks, Erkka Rouhe, Jaakko Seppänen, Manuel Martín-Neira
2019 J jnl
CoRR
Sanja Scepanovic, Oleg Antropov, Pekka Laurila, Vladimir Ignatenko, Jaan Praks
2018 C conf
IGARSS
Marjan Marbouti, Oleg Antropov, Patrick Eriksson, Jaan Praks, Vahid Arabzadeh, Eero Rinne, Matti Leppdar Nta
2018 C conf
IGARSS
Martti Hallikainen, Matti Vaaja, Jaakko Seppänen, Jaan Praks
2018 C conf
IGARSS
Jaan Praks, Oleg Antropov, Aire Olesk, Kaupo Voormansik
2018 C conf
IGARSS
Juha Kainulainen, Sampo Salo, Janne Lahtinen, Guifré Molera Calvés, Jaakko Seppänen, Jaan Praks, Teemu Hakala, Yuwei Chen, Juha Hyyppä, Martin Unwin, Philip Jales, Gerhard Ressler, Tania Casal, Josep Roselló
2018 C conf
IGARSS
Jaan Praks, Petri Niemela, Antti Näsilä, Antti Kestila, Nemanja Jovanovic, Bagus Adiwiluhung Riwanto, Tuomas Tikka, Hannu Leppinen, Rami Vainio, Pekka Janhunen
2018 C conf
IGARSS
Oleg Antropov, Jaakko Seppänen, Martti Hallikainen, Jaan Praks, Thomas Jagdhuber
2018 C conf
IGARSS
Oleg Antropov, Yrjö Rauste, Katri Tegel, Yamuna Baral, Virpi Junttila, Tuomo Kauranne, Tuomas Häme, Jaan Praks
2018 C conf
IGARSS
Silvan Leinss, Oleg Antropov, Juho Vehvilainen, Juha Lemmetyinen, Irena Hajnsek, Jaan Praks
2017 J jnl
Remote. Sens.
Marjan Marbouti, Jaan Praks, Oleg Antropov, Eero Rinne, Matti Leppäranta
2017 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Martti T. Hallikainen, Jaan Praks, Jiancheng Shi, Andreas Colliander
2017 J jnl
IEEE Geosci. Remote. Sens. Lett.
Jaakko Seppänen, Oleg Antropov, Thomas Jagdhuber, Martti Hallikainen, Janne Heiskanen, Jaan Praks
2017 J jnl
IEEE Geosci. Remote. Sens. Lett.
Jaakko Seppänen, Jaan Praks, Pauli Sievinen, Anssi Hakkarainen, Martti Hallikainen
2017 C conf
IGARSS
Thomas Jagdhuber, Martin J. Baur, Moritz Link, Maria Piles, Dara Entekhabi, Carsten Montzka, Jaakko Seppänen, Oleg Antropov, Jaan Praks, Alexander Löw
2017 J jnl
IEEE Trans. Aerosp. Electron. Syst.
Bagus Adiwiluhung Riwanto, Tuomas Tikka, Antti Kestila, Jaan Praks
2017 J jnl
Remote. Sens.
Oleg Antropov, Yrjö Rauste, Tuomas Häme, Jaan Praks
2017 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Jaakko Seppänen, Juho Jakkila, Jaan Praks, Martti T. Hallikainen
2016 C conf
IGARSS
Jaan Praks, Aire Olesk, Kaupo Voormansik, Oleg Antropov, Karlis Zalite, Mart Noorma
2016 C conf
IGARSS
Jaakko Seppänen, Jaan Praks, Oleg Antropov
2016 J jnl
Remote. Sens.
Aire Olesk, Jaan Praks, Oleg Antropov, Karlis Zalite, Tauri Arumäe, Kaupo Voormansik
2016 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Jaakko Seppänen, Juha Kainulainen, Janne Heiskanen, Jaan Praks, Martti Hallikainen
2016 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Karlis Zalite, Oleg Antropov, Jaan Praks, Kaupo Voormansik, Mart Noorma
2016 C conf
IGARSS
Fran Fabra, Estel Cardellach, Sernerni Ribo, Weiqiang Li, Antonio Rius, Jaan Praks, Erkka Rouhe, Jaakko Seppänen, Manuel Martín-Neira
2015 C conf
IGARSS
Jaan Praks, Antti Kestila, Tuomas Tikka, Hannu Leppinen, O. Khurshid, Martti Hallikainen
2015 C conf
IGARSS
Oleg Antropov, Yrjö Rauste, Tuomas Häme, Jaan Praks
2015 C conf
IGARSS
Martti Hallikainen, Juha Lemmetyinen, Matti Vaaja, Jaakko Seppänen, Jaan Praks
2015 C conf
EDUCON
Jaan Praks, Tuomas Tikka, Antti Kestila, Maria Hieta
2015 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Aire Olesk, Kaupo Voormansik, Ants Vain, Mart Noorma, Jaan Praks
2015 C conf
IGARSS
Kalev Koppel, Karlis Zalite, Anni Sisas, Kaupo Voormansik, Jaan Praks, Mart Noorma
2014 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Kaupo Voormansik, Jaan Praks, Oleg Antropov, Jüri Jagomägi, Karlis Zalite
2014 J jnl
IEEE Trans. Geosci. Remote. Sens.
Oleg Antropov, Yrjö Rauste, Heikki Astola, Jaan Praks, Tuomas Häme, Martti T. Hallikainen
2014 C conf
IGARSS
Karlis Zalite, Kaupo Voormansik, Jaan Praks, Oleg Antropov, Mart Noorma
2012 C conf
IGARSS
Jaan Praks, Martti Hallikainen, Oleg Antropov, Daniel Molina
2012 J jnl
IEEE Trans. Geosci. Remote. Sens.
Jaan Praks, Oleg Antropov, Martti T. Hallikainen
2012 J jnl
IEEE J. Sel. Top. Appl. Earth Obs. Remote. Sens.
Andreas Tack, Jarkko Koskinen, Antti Hellsten, Pauli Sievinen, Igor Esau, Jaan Praks, Jaakko Kukkonen, Martti Hallikainen
2012 C conf
IGARSS
Oleg Antropov, Yrjö Rauste, Jaan Praks, Martti Hallikainen, Tuomas Häme
2011 C conf
IGARSS
Jaan Praks, Antti Kestila, Martti Hallikainen, Heikki Saari, Jarkko Antila, Pekka Janhunen, Rami Vainio
2010 C conf
IGARSS
Annakaisa von Lerber, Timo Piepponen, Jarkko Koskinen, Dmitri Moisseev, Dmitri Kestilä, Jani Tyynela, Timo Nousiainen, Jarmo Koistinen, Ari Sihvola, Pasi Ylä-Oijala, Jaan Praks, Martti Hallikainen, Jouni Pulliainen
2010 C conf
IGARSS
Jaan Praks, Martti Hallikainen, Elise Colin Koeniguer
2010 J jnl
IEEE Trans. Geosci. Remote. Sens.
Tommi Dufva, Jaan Praks, Seppo Järvenpää, Jukka Sarvas
2009 J jnl
IEEE Trans. Geosci. Remote. Sens.
Jaan Praks, Elise Colin Koeniguer, Martti T. Hallikainen
2009 conf
IGARSS (5)
Jaan Praks, Martti Hallikainen, Juha Hyyppä, Jaakko Seppänen
2009 conf
IGARSS (3)
Pauli Sievinen, Jaan Praks, Jarkko Koskinen, Martti Hallikainen, Jaakko Kukkonen, Antti Hellsten
2009 J jnl
IEEE Geosci. Remote. Sens. Lett.
Jaan Praks, Jukka Sarvas, Martti Hallikainen, Jouni Pulliainen
2008 conf
IGARSS (2)
Jaan Praks, Florian Kugler, Juha Hyyppä, Konstantinos Papathanassiou, Martti Hallikainen
2007 J jnl
IEEE Geosci. Remote. Sens. Lett.
Jaan Praks, Florian Kugler, Konstantinos P. Papathanassiou, Irena Hajnsek, Martti Hallikainen
2007 C conf
IGARSS
Jaan Praks, Martti Hallikainen, Florian Kugler, Konstantinos P. Papathanassiou
2006 C conf
IGARSS
Jaan Praks, Martti Hallikainen, Florian Kugler, Kostas Papathanassiou, Irena Hajnsek
2004 C conf
IGARSS
Jaan Praks, Miia Eskelinen, Jouni Pulliainen, Timo Pyhälahti, Martti Hallikainen
2004 C conf
IGARSS
Miia Eskelinen, Jouni Pulliainen, Jaan Praks, Martti Hallikainen
2003 C conf
IGARSS
Jaan Praks, Jouni Pulliainen, Pekka Ahtonen, Martti Hallikainen
2003 C conf
IGARSS
Miia Eskelinen, Sari Metsämäki, Jouni Pulliainen, Martti Hallikainen, Jaan Praks
1999 J jnl
IEEE Trans. Geosci. Remote. Sens.
Nerijus Kruopis, Jaan Praks, Ali Nadir Arslan, Hanna M. Alasalmi, Jarkko Koskinen, Martti T. Hallikainen
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)