James Allan

281 papers A* 68A 53B 3C 1Misc 18Journal 64Unranked 73
YearRankTypeTitle / Venue / Authors
2025 J jnl
CoRR
Tanya Chowdhury, Atharva Nijasure, Yair Zick, James Allan
2025 J jnl
CoRR
Atharva Nijasure, Tanya Chowdhury, James Allan
2025 Misc conf
ICTIR
Tanya Chowdhury, Atharva Nijasure, James Allan
2025 A* conf
ICLR
Tanya Chowdhury, Yair Zick, James Allan
2025 conf
UMAP (Adjunct Publication)
Nazanin Jafari, James Allan
2025 J jnl
SIGIR Forum
Johanne R. Trippas, J. Shane Culpepper, Mohammad Aliannejadi, James Allan, Enrique Amigó, Jaime Arguello, Leif Azzopardi, Peter Bailey, Jamie Callan, Rob Capra, Nick Craswell, W. Bruce Croft, Jeff Dalton, Gianluca Demartini, Laura Dietz, Zhicheng Dou, Carsten Eickhoff, Michael D. Ekstrand, Nicola Ferro, Norbert Fuhr, Dorota Glowacka, Faegheh Hasibi, Danula Hettiachchi, Rosie Jones, Jaap Kamps, Noriko Kando, Sarvnaz Karimi, Makoto P. Kato, Bevan Koopman, Yiqun Liu, Chenglong Ma, Joel Mackenzie, Maria Maistro, Jiaxin Mao, Dana McKay, Bhaskar Mitra, Stefano Mizzaro, Alistair Moffat, Josiane Mothe, Iadh Ounis, Lida Rashidi, Yongli Ren, Mark Sanderson, Rodrygo L. T. Santos, Falk Scholer, Chirag Shah, Laurianne Sitbon, Ian Soboroff, Damiano Spina, Paul Thomas, Julián Urbano, Arjen P. de Vries, Ryen White, Abby Yuan, Hamed Zamani, Oleg Zendel, Min Zhang, Shengyao Zhuang, Justin Zobel, Guido Zuccon
2024 A* conf
EMNLP
Youngwoo Kim, Razieh Rahimi, James Allan
2024 J jnl
CoRR
Youngwoo Kim, Razieh Rahimi, James Allan
2024 J jnl
CoRR
James Allan, Eunsol Choi, Daniel P. Lopresti, Hamed Zamani
2024 A* conf
EMNLP
Zhiqi Huang, Puxuan Yu, Shauli Ravfogel, James Allan
2024 J jnl
CoRR
Tanya Chowdhury, James Allan
2024 J jnl
CoRR
Tanya Chowdhury, Yair Zick, James Allan
2024 J jnl
CoRR
Nazanin Jafari, James Allan
2024 Misc conf
ICTIR
Nazanin Jafari, James Allan, Sheikh Muhammad Sarwar
2024 J jnl
CoRR
Nazanin Jafari, James Allan
2023 A conf
EACL
Dong-Ho Lee, Ravi Kiran Selvam, Sheikh Muhammad Sarwar, Bill Yuchen Lin, Fred Morstatter, Jay Pujara, Elizabeth Boschee, James Allan, Xiang Ren
2023 conf
EMNLP (Findings)
Youngwoo Kim, Razieh Rahimi, James Allan
2023 A* conf
SIGIR
Negin Ghasemi, Mohammad Aliannejadi, Hamed R. Bonab, Evangelos Kanoulas, Arjen P. de Vries, James Allan, Djoerd Hiemstra
2023 J jnl
CoRR
Zhiqi Huang, Puxuan Yu, James Allan
2023 J jnl
CoRR
Ali Montazeralghaem, James Allan
2023 Misc conf
ICTIR
Zhiqi Huang, Shahrzad Naseri, Hamed R. Bonab, Sheikh Muhammad Sarwar, James Allan
2023 A conf
WSDM
Zhiqi Huang, Puxuan Yu, James Allan
2023 J jnl
CoRR
Zhiqi Huang, Puxuan Yu, James Allan
2023 Misc conf
ICTIR
Tanya Chowdhury, Razieh Rahimi, James Allan
2023 A conf
CIKM
Puxuan Yu, Razieh Rahimi, Zhiqi Huang, James Allan
2023 A* conf
SIGIR
Zhiqi Huang, Hansi Zeng, Hamed Zamani, James Allan
2023 J jnl
CoRR
Zhiqi Huang, Hansi Zeng, Hamed Zamani, James Allan
2023 conf
TREC
Zhiqi Huang, Puxuan Yu, James Allan
2023 J jnl
CoRR
Abhishek Madaan, Tanya Chowdhury, Neha Rana, James Allan, Tanmoy Chakraborty
2022 J jnl
ACM Trans. Inf. Syst.
Edward Kai Fung Dang, Robert Wing Pong Luk, James Allan
2022 J jnl
J. Assoc. Inf. Sci. Technol.
Edward Kai Fung Dang, Robert Wing Pong Luk, James Allan
2022 A* conf
SIGIR
Youngwoo Kim, Razieh Rahimi, James Allan
2022 J jnl
Inf. Retr. J.
Shahrzad Naseri, Jeffrey Dalton, Andrew Yates, James Allan
2022 conf
FAccT
Tanya Chowdhury, Razieh Rahimi, James Allan
2022 A* conf
KDD
Ali Montazeralghaem, James Allan
2022 A conf
WSDM
Ali Montazeralghaem, James Allan
2022 J jnl
CoRR
Tanya Chowdhury, Razieh Rahimi, James Allan
2022 A* conf
SIGIR
Puxuan Yu, Razieh Rahimi, James Allan
2021 J jnl
IEEE Trans. Fuzzy Syst.
Edward Kai Fung Dang, Robert Wing Pong Luk, James Allan
2021 A* conf
SIGIR
Zhiqi Huang, Razieh Rahimi, Puxuan Yu, Jingbo Shang, James Allan
2021 J jnl
CoRR
Dong-Ho Lee, Ravi Kiran Selvam, Sheikh Muhammad Sarwar, Bill Yuchen Lin, Mahak Agarwal, Fred Morstatter, Jay Pujara, Elizabeth Boschee, James Allan, Xiang Ren
2021 conf
ECIR (1)
Shahrzad Naseri, Jeff Dalton, Andrew Yates, James Allan
2021 J jnl
CoRR
Shahrzad Naseri, Jeffrey Dalton, Andrew Yates, James Allan
2021 A conf
CIKM
Hamed R. Bonab, Mohammad Aliannejadi, Ali Vardasbi, Evangelos Kanoulas, James Allan
2021 J jnl
CoRR
Hamed R. Bonab, Mohammad Aliannejadi, Ali Vardasbi, Evangelos Kanoulas, James Allan
2021 J jnl
CoRR
Razieh Rahimi, Youngwoo Kim, Hamed Zamani, James Allan
2021 conf
INRA@RecSys
Masaharu Yoshioka, Norihiko Tatsunami, Masahiko Itoh, Noriko Kando, James Allan
2021 A conf
RecSys
Ali Montazeralghaem, James Allan, Philip S. Thomas
2021 A conf
CIKM
Zhiqi Huang, Hamed R. Bonab, Sheikh Muhammad Sarwar, Razieh Rahimi, James Allan
2021 J jnl
CoRR
Zhiqi Huang, Hamed R. Bonab, Sheikh Muhammad Sarwar, Razieh Rahimi, James Allan
2021 Misc conf
ICTIR
Lakshmi Vikraman, Ali Montazeralghaem, Helia Hashemi, W. Bruce Croft, James Allan
2021 A conf
CIKM
Youngwoo Kim, Razieh Rahimi, Hamed R. Bonab, James Allan
2021 J jnl
CoRR
Youngwoo Kim, Razieh Rahimi, Hamed R. Bonab, James Allan
2021 A* conf
SIGIR
Sheikh Muhammad Sarwar, Felipe Moraes, Jiepu Jiang, James Allan
2021 A conf
RecSys
Mohammad Aliannejadi, Hamed R. Bonab, Ali Vardasbi, Evangelos Kanoulas, James Allan, Vanessa Murdock
2020 A* conf
SIGIR
Ali Montazeralghaem, Hamed Zamani, James Allan
2020 A* conf
SIGIR
Puxuan Yu, James Allan
2020 J jnl
CoRR
Puxuan Yu, James Allan
2020 J jnl
Int. J. Softw. Eng. Knowl. Eng.
Edward Kai Fung Dang, Robert Wing Pong Luk, James Allan
2020 J jnl
ACM Trans. Inf. Syst.
Youngwoo Kim, Myungha Jang, James Allan
2020 Misc conf
ICTIR
Puxuan Yu, Razieh Rahimi, Zhiqi Huang, James Allan
2020 A* conf
SIGIR
Sheikh Muhammad Sarwar, James Allan
2020 conf
ECIR (1)
Ali Montazeralghaem, Razieh Rahimi, James Allan
2020 Misc conf
ICTIR
Sheikh Muhammad Sarwar, Raghavendra Addanki, Ali Montazeralghaem, Soumyabrata Pal, James Allan
2020 A* conf
SIGIR
Hamed R. Bonab, Sheikh Muhammad Sarwar, James Allan
2019 conf
ACL (1)
Sheikh Muhammad Sarwar, Hamed R. Bonab, James Allan
2019 J jnl
CoRR
Sheikh Muhammad Sarwar, Hamed R. Bonab, James Allan
2019 A* conf
SIGIR
Puxuan Yu, Zhiqi Huang, Razieh Rahimi, James Allan
2019 Misc conf
ICTIR
Hamed R. Bonab, Mohammad Aliannejadi, John Foley, James Allan
2019 Misc conf
ICTIR
Razieh Rahimi, Ali Montazeralghaem, James Allan
2019 conf
ECIR (1)
Jeffrey Dalton, Shahrzad Naseri, Laura Dietz, James Allan
2019 Misc ed.
ICTIR
Yi Fang, Yi Zhang, James Allan, Krisztian Balog, Ben Carterette, Jiafeng Guo
2019 Misc conf
ICTIR
Sheikh Muhammad Sarwar, James Allan
2019 J jnl
CoRR
Shahrzad Naseri, Sheikh Muhammad Sarwar, James Allan
2019 Misc conf
ICTIR
Sheikh Muhammad Sarwar, John Foley, Liu Yang, James Allan
2019 Misc conf
ICTIR
Hamed R. Bonab, James Allan, Ramesh K. Sitaraman
2019 Misc conf
ICTIR
Ali Montazeralghaem, Razieh Rahimi, James Allan
2019 conf
ECIR (1)
Youngwoo Kim, James Allan
2018 A* conf
SIGIR
Hamed R. Bonab, Hamed Zamani, Erik G. Learned-Miller, James Allan
2018 A* conf
SIGIR
Myungha Jang, James Allan
2018 J jnl
CoRR
Myungha Jang, James Allan
2018 conf
CIKM Workshops
Shahrzad Naseri, John Foley, James Allan, Brendan T. O'Connor
2018 J jnl
CoRR
John Foley, Sheikh Muhammad Sarwar, James Allan
2018 A ed.
CIKM
Alfredo Cuzzocrea, James Allan, Norman W. Paton, Divesh Srivastava, Rakesh Agrawal, Andrei Z. Broder, Mohammed J. Zaki, K. Selçuk Candan, Alexandros Labrinidis, Assaf Schuster, Haixun Wang
2018 B conf
CHIIR
Sheikh Muhammad Sarwar, John Foley, James Allan
2018 J jnl
CoRR
Sheikh Muhammad Sarwar, John Foley, James Allan
2018 conf
TREC
Shahrzad Naseri, John Foley, James Allan
2018 A* conf
SIGIR
Daniel Cohen, John Foley, Hamed Zamani, James Allan, W. Bruce Croft
2018 conf
NewsIR@ECIR
Masaharu Yoshioka, Myungha Jang, James Allan, Noriko Kando
2017 A conf
CIKM
Jiepu Jiang, James Allan
2017 A* conf
SIGIR
Jiepu Jiang, Daqing He, James Allan
2017 J jnl
CoRR
Myungha Jang, Jinho D. Choi, James Allan
2017 conf
NUT@EMNLP
Myungha Jang, Jinho D. Choi, James Allan
2017 J jnl
CoRR
Shiri Dori-Hacohen, Myungha Jang, James Allan
2017 Misc conf
ICTIR
Myungha Jang, Shiri Dori-Hacohen, James Allan
2017 J jnl
SIGIR Forum
James Allan, Ron Papka, Victor Lavrenko
2017 J jnl
SIGIR Forum
James Allan, Nicholas J. Belkin, Paul N. Bennett, Jamie Callan, Charles L. A. Clarke, Fernando Diaz, Susan T. Dumais, Nicola Ferro, Donna Harman, Djoerd Hiemstra, Ian Ruthven, Tetsuya Sakai, Mark D. Smucker, Justin Zobel
2017 A conf
CIKM
Jiepu Jiang, James Allan
2017 conf
TREC
James Allan, Donna Harman, Evangelos Kanoulas, Dan Li, Christophe Van Gysel, Ellen M. Voorhees
2017 conf
TREC
Qingyao Ai, Hamed Zamani, Stephen M. Harding, Shahrzad Naseri, James Allan, W. Bruce Croft
2017 B conf
CHIIR
Jiepu Jiang, Daqing He, Diane Kelly, James Allan
2016 J jnl
J. Assoc. Inf. Sci. Technol.
Edward Kai Fung Dang, Robert W. P. Luk, James Allan
2016 A conf
ECIR
Jiepu Jiang, James Allan
2016 A* conf
SIGIR
Shiri Dori-Hacohen, David D. Jensen, James Allan
2016 B conf
CHIIR
Jiepu Jiang, James Allan
2016 A* conf
SIGIR
Myungha Jang, James Allan
2016 A conf
CIKM
John Foley, Brendan O'Connor, James Allan
2016 A conf
ECIR
Nada Naji, James Allan
2016 A conf
CIKM
Weize Kong, James Allan
2016 A conf
CIKM
Myungha Jang, John Foley, Shiri Dori-Hacohen, James Allan
2016 A conf
WSDM
Jiepu Jiang, James Allan
2016 A conf
ECIR
John Foley, James Allan
2015 A conf
ECIR
Shiri Dori-Hacohen, James Allan
2015 Misc conf
ICTIR
David Wemhoener, James Allan
2015 J jnl
ACM Trans. Inf. Syst.
Edward Kai Fung Dang, Robert Wing Pong Luk, James Allan
2015 conf
SCST@ECIR
Shiri Dori-Hacohen, Elad Yom-Tov, James Allan
2015 A* conf
SIGIR
Weize Kong, Rui Li, Jie Luo, Aston Zhang, Yi Chang, James Allan
2015 Misc ed.
ICTIR
James Allan, W. Bruce Croft, Arjen P. de Vries, ChengXiang Zhai
2015 A conf
ECIR
John Foley, James Allan
2014 J jnl
J. Assoc. Inf. Sci. Technol.
Edward K. F. Dang, Robert W. P. Luk, James Allan
2014 A* conf
SIGIR
Jeffrey Dalton, Laura Dietz, James Allan
2014 A conf
CIKM
Weize Kong, James Allan
2014 A* conf
SIGIR
Jiepu Jiang, James Allan
2014 conf
TRECVID
Hui Cheng, Jingen Liu, Ishani Chakraborty, Guang Chen, Qiguang Liu, Mohamed Elhoseiny, Gary Gan, Ajay Divakaran, Harpreet S. Sawhney, James Allan, John Foley, Mubarak Shah, Afshin Dehghan, Michael Witbrock, Jon Curtis
2014 A* conf
SIGIR
Jiepu Jiang, Daqing He, James Allan
2013 A conf
CIKM
Shiri Dori-Hacohen, James Allan
2013 A* conf
SIGIR
Weize Kong, James Allan
2013 A conf
CIKM
Weize Kong, Elif Aktolga, James Allan
2013 conf
TRECVID
Jingen Liu, Hui Cheng, Omar Javed, Qian Yu, Ishani Chakraborty, Weiyu Zhang, Ajay Divakaran, Harpreet S. Sawhney, James Allan, R. Manmatha, John Foley, Mubarak Shah, Afshin Dehghan, Michael Witbrock, Jon Curtis, Gerald Friedland
2013 A* conf
SIGIR
Elif Aktolga, James Allan
2013 conf
TRECVID
James Allan, Jeff Dalton, John Foley, R. Manmatha, Venkatesh N. Murthy, David Wemhoener
2013 A* conf
SIGIR
Henry Allen Feild, James Allan
2013 conf
LivingLab@CIKM
Henry Allen Feild, James Allan
2013 A conf
CIKM
Jeffrey Dalton, James Allan, Pranav Mirajkar
2012 A* conf
SIGIR
Marc-Allen Cartright, Ethem F. Can, William Dabney, Jeff Dalton, Logan Giorda, Kriste Krstovski, Xiaoye Wu, Ismet Zeki Yalniz, James Allan, R. Manmatha, David A. Smith
2012 J jnl
SIGIR Forum
James Allan, W. Bruce Croft, Alistair Moffat, Mark Sanderson
2012 conf
IIiX
Mark D. Smucker, James Allan, Blagovest Dachev
2012 conf
TRECVID
Hui Cheng, Jingen Liu, Saad Ali, Omar Javed, Qian Yu, Amir Tamrakar, Ajay Divakaran, Harpreet S. Sawhney, R. Manmatha, James Allan, Alexander G. Hauptmann, Mubarak Shah, Subhabrata Bhattacharya, Afshin Dehghan, Gerald Friedland, Benjamin Elizalde, Trevor Darrell, Michael Witbrock, Jon Curtis
2012 conf
BooksOnline@CIKM
Marc-Allen Cartright, Jeffrey Dalton, James Allan
2012 A* conf
SIGIR
Henry Allen Feild, James Allan
2011 A* conf
SIGIR
Henry Allen Feild, James Allan, Joshua Glatt
2011 A conf
CIKM
Xing Yi, James Allan
2011 A conf
CIKM
Marc-Allen Cartright, James Allan
2011 conf
BooksOnline
Marc-Allen Cartright, Henry Allen Feild, James Allan
2011 conf
BooksOnline
David A. Smith, R. Manmatha, James Allan
2011 A conf
ECIR
Elif Aktolga, James Allan, David A. Smith
2011 A conf
CIKM
Jeffrey Dalton, James Allan, David A. Smith
2011 A conf
CIKM
Elif Aktolga, James Allan
2011 conf
TRECVID
Hui Cheng, Amir Tamrakar, Saad Ali, Qian Yu, Omar Javed, Jingen Liu, Ajay Divakaran, Harpreet S. Sawhney, Alexander G. Hauptmann, Mubarak Shah, Subhabrata Bhattacharya, Michael Witbrock, Jon Curtis, Gerald Friedland, Robert Mertens, Trevor Darrell, R. Manmatha, James Allan
2011 conf
INEX
Henry Allen Feild, Marc-Allen Cartright, James Allan
2010 A* conf
SIGIR
Xing Yi, James Allan
2010 J jnl
J. Assoc. Inf. Sci. Technol.
Edward K. F. Dang, Robert W. P. Luk, James Allan, Kei Shiu Ho, Stephen C. F. Chan, Korris Fu-Lai Chung, Dik Lun Lee
2010 A conf
CIKM
Marc-Allen Cartright, James Allan, Victor Lavrenko, Andrew McGregor
2010 A* conf
SIGIR
Niranjan Balasubramanian, James Allan
2010 A* conf
SIGIR
Henry Allen Feild, James Allan, Rosie Jones
2009 A conf
ECIR
Xing Yi, James Allan
2009 Misc conf
ICTIR
Mark D. Smucker, James Allan
2009 A* conf
SIGIR
Mark D. Smucker, James Allan, Ben Carterette
2009 A conf
ECIR
Ben Carterette, Virgiliu Pavlu, Evangelos Kanoulas, Javed A. Aslam, James Allan
2009 A conf
CIKM
Ao Feng, James Allan
2009 A* ed.
SIGIR
James Allan, Javed A. Aslam, Mark Sanderson, ChengXiang Zhai, Justin Zobel
2009 A conf
ECIR
Matthew Lease, James Allan, W. Bruce Croft
2009 C conf
SPIRE
Niranjan Balasubramanian, James Allan
2008 A* conf
SIGIR
Kyung-Soon Lee, W. Bruce Croft, James Allan
2008 A* ed.
ACL
Kathleen R. McKeown, Johanna D. Moore, Simone Teufel, James Allan, Sadaoki Furui
2008 J jnl
Inf. Process. Manag.
Giridhar Kumaran, James Allan
2008 A conf
CIKM
Elif Aktolga, Marc-Allen Cartright, James Allan
2008 A* conf
SIGIR
Giridhar Kumaran, James Allan
2008 conf
TREC
Ho Chung Wu, Edward K. F. Dang, Robert Wing Pong Luk, G. Ngai, Yinghao Li, James Allan, Kui-Lam Kwok, Kam-Fai Wong
2008 A conf
CIKM
Xing Yi, James Allan
2008 A* conf
SIGIR
Ben Carterette, Virgiliu Pavlu, Evangelos Kanoulas, Javed A. Aslam, James Allan
2008 conf
eScience
Rebecca Reznik-Zellen, Bob Stevens, Michael Thorn, Jeff Morse, Mark D. Smucker, James Allan, David M. Mimno, Andrew McCallum, Mark Tuominen
2008 A conf
CIKM
Lisa Friedland, James Allan
2008 conf
TREC
James Allan, Javed A. Aslam, Virgil Pavlu, Evangelos Kanoulas, Ben Carterette
2008 A conf
CIKM
Kristen Parton, Kathleen R. McKeown, James Allan, Enrique Henestroza
2007 conf
HLT-NAACL
Giridhar Kumaran, James Allan
2007 A* conf
SIGIR
Niranjan Balasubramanian, James Allan, W. Bruce Croft
2007 A conf
CIKM
Mark D. Smucker, James Allan, Ben Carterette
2007 A* conf
SIGIR
Hema Raghavan, James Allan
2007 conf
RIAO
Xing Yi, James Allan, Victor Lavrenko
2007 A conf
CIKM
Ao Feng, James Allan
2007 J jnl
Int. J. Document Anal. Recognit.
E. Micah Kornfield, R. Manmatha, James Allan
2007 conf
TREC
Xing Yi, James Allan
2007 conf
HLT-NAACL
Victor Lavrenko, Xing Yi, James Allan
2007 conf
RIAO
Giridhar Kumaran, James Allan
2007 A* conf
IJCAI
Ron Bekkerman, Hema Raghavan, James Allan, Koji Eguchi
2007 A* conf
SIGIR
Xing Yi, James Allan, W. Bruce Croft
2007 J jnl
SIGIR Forum
Jamie Callan, James Allan, Charles L. A. Clarke, Susan T. Dumais, David A. Evans, Mark Sanderson, ChengXiang Zhai
2007 conf
TREC
James Allan, Ben Carterette, Blagovest Dachev, Javed A. Aslam, Virgiliu Pavlu, Evangelos Kanoulas
2007 conf
HLT-NAACL
Barry Schiffman, Kathleen R. McKeown, Ralph Grishman, James Allan
2007 conf
RIAO
Ben Carterette, James Allan
2007 A conf
CIKM
Giridhar Kumaran, James Allan
2007 A conf
CIKM
Ben Carterette, James Allan
2007 conf
TREC
Mark D. Smucker, James Allan, Blagovest Dachev
2007 A* conf
SIGIR
Mark D. Smucker, James Allan
2007 A* conf
IJCAI
Ron Bekkerman, Shlomo Zilberstein, James Allan
2006 A* conf
SIGIR
Mark D. Smucker, James Allan
2006 A* conf
SIGIR
Mark D. Smucker, James Allan
2006 A* conf
SIGIR
Ben Carterette, James Allan, Ramesh K. Sitaraman
2006 A* conf
SIGIR
Giridhar Kumaran, James Allan
2006 conf
TREC
Giridhar Kumaran, James Allan
2005 conf
TREC
James Allan
2005 A conf
CIKM
Ben Carterette, James Allan
2005 conf
HLT/EMNLP
Hema Raghavan, James Allan
2005 conf
HICSS
James Allan, Stephen M. Harding, David Fisher, Alvaro Bolivar, Sergio Guzman-Lara, Peter Amstutz
2005 conf
HLT/EMNLP
Giridhar Kumaran, James Allan
2005 conf
TREC
Fernando Diaz, James Allan
2005 A* conf
SIGIR
James Allan, Ben Carterette, Joshua Lewis
2004 A conf
ECIR
Diane Kelly, Fernando Diaz, Nicholas J. Belkin, James Allan
2004 conf
HLT-NAACL
Chung Heong Gooi, James Allan
2004 ed.
DRR
Elisa H. Barney Smith, Jianying Hu, James Allan
2004 J jnl
J. Intell. Inf. Syst.
Andrew Arnt, Shlomo Zilberstein, James Allan, Abdel-Illah Mouaddib
2004 A conf
CIKM
Ramesh Nallapati, Ao Feng, Fuchun Peng, James Allan
2004 conf
TREC
James Allan
2004 J jnl
User Model. User Adapt. Interact.
Anton Leuski, James Allan
2004 A* ed.
SIGIR
Mark Sanderson, Kalervo Järvelin, James Allan, Peter Bruza
2004 conf
DIAL
E. Micah Kornfield, R. Manmatha, James Allan
2004 A* conf
SIGIR
Giridhar Kumaran, James Allan
2004 conf
TREC
Nasreen Abdul Jaleel, James Allan, W. Bruce Croft, Fernando Diaz, Leah S. Larkey, Xiaoyan Li, Mark D. Smucker, Courtney Wade
2003 J jnl
ACM Trans. Asian Lang. Inf. Process.
James Allan, Victor Lavrenko, Margaret E. Connell
2003 J jnl
SIGIR Forum
James Allan, Jay Aslam, Nicholas J. Belkin, Chris Buckley, James P. Callan, W. Bruce Croft, Susan T. Dumais, Norbert Fuhr, Donna Harman, David J. Harper, Djoerd Hiemstra, Thomas Hofmann, Eduard H. Hovy, Wessel Kraaij, John D. Lafferty, Victor Lavrenko, David D. Lewis, Liz Liddy, R. Manmatha, Andrew McCallum, Jay M. Ponte, John M. Prager, Dragomir R. Radev, Philip Resnik, Stephen E. Robertson, Ronald Rosenfeld, Salim Roukos, Mark Sanderson, Richard M. Schwartz, Amit Singhal, Alan F. Smeaton, Howard R. Turtle, Ellen M. Voorhees, Ralph M. Weischedel, Jinxi Xu, ChengXiang Zhai
2003 A conf
CIKM
James Allan, Ao Feng, Alvaro Bolivar
2003 conf
TREC
James Allan
2003 A conf
CIKM
Ramesh Nallapati, W. Bruce Croft, James Allan
2003 A* conf
SIGIR
James Allan, Courtney Wade, Alvaro Bolivar
2003 J jnl
EURASIP J. Adv. Signal Process.
James Allan
2003 A* conf
SIGIR
James Allan, Giridhar Kumaran
2003 conf
TREC
Nasreen Abdul Jaleel, Andrés Corrada-Emmanuel, Qi Li, Xiaoyong Liu, Courtney Wade, James Allan
2002 A* conf
SIGIR
R. Manmatha, Ao Feng, James Allan
2002 J jnl
J. Assoc. Inf. Sci. Technol.
Robert W. P. Luk, Hong Va Leong, Tharam S. Dillon, Alvin T. S. Chan, W. Bruce Croft, James Allan
2002 A conf
CIKM
Ramesh Nallapati, James Allan
2002 J jnl
CoRR
Carl Lagoze, William Y. Arms, Stoney Gan, Diane Hillmann, Christopher Ingram, Dean B. Krafft, Richard J. Marisa, Jon Phipps, John Saylor, Carol Terrizzi, Walter Hoehn, David Millman, James Allan, Sergio Guzman-Lara, Tom Kalt
2002 conf
JCDL
Carl Lagoze, William Y. Arms, Stoney Gan, Diane Hillmann, Christopher Ingram, Dean B. Krafft, Richard J. Marisa, Jon Phipps, John Saylor, Carol Terrizzi, Walter Hoehn, David Millman, James Allan, Sergio Guzman-Lara, Tom Kalt
2002 J jnl
Inf. Retr.
James Allan
2002 A* conf
SIGIR
Anton Leuski, James Allan
2002 conf
TREC
Leah S. Larkey, James Allan, Margaret E. Connell, Alvaro Bolivar, Courtney Wade
2002 A* conf
SIGIR
James Allan, Hema Raghavan
2001 conf
HLT
Vikas Khandelwal, Rahul Gupta, James Allan
2001 J jnl
Inf. Process. Manag.
James Allan, Anton Leuski, Russell C. Swan, Donald Byrd
2001 conf
HLT
David Frey, Rahul Gupta, Vikas Khandelwal, Victor Lavrenko, Anton Leuski, James Allan
2001 conf
SIGIR Workshop: Information Retrieval Techniques for Speech Applications
James Allan
2001 A* conf
SIGIR
James Allan, Rahul Gupta, Vikas Khandelwal
2000 A* conf
SIGIR
Russell C. Swan, James Allan
2000 A conf
CIKM
James Allan, Victor Lavrenko, Hubert Jin
2000 conf
TREC
James Allan, Margaret E. Connell, W. Bruce Croft, Fangfang Feng, David Fisher, Xiaoyan Li
2000 conf
RIAO
Anton Leuski, James Allan
2000 A conf
CIKM
Victor Lavrenko, Matthew D. Schmill, Dawn J. Lawrie, Paul Ogilvie, David D. Jensen, James Allan
2000 conf
INFOVIS
Anton Leuski, James Allan
2000 J jnl
Int. J. Digit. Libr.
Anton Leuski, James Allan
2000 A* conf
SIGIR
Russell C. Swan, James Allan
1999 A conf
CIKM
Russell C. Swan, James Allan
1999 conf
TREC
James Allan, James P. Callan, Fangfang Feng, Daniella Malin
1998 A* conf
SIGIR
Russell C. Swan, James Allan
1998 J jnl
Inf. Process. Manag.
James Allan
1998 A conf
CIKM
Ron Papka, James Allan
1998 conf
ECDL
Anton Leuski, James Allan
1998 conf
TREC
James Allan, James P. Callan, Mark Sanderson, Jinxi Xu, Steven Wegmann
1998 A* conf
SIGIR
James Allan, Ron Papka, Victor Lavrenko
1998 J jnl
Multim. Tools Appl.
Daniela Rus, James Allan
1998 A* conf
SIGIR
Anton Leuski, James Allan
1997 J jnl
Inf. Process. Manag.
James Allan
1997 conf
TREC
James Allan, James P. Callan, W. Bruce Croft, Lisa Ballesteros, Donald Byrd, Russell C. Swan, Jinxi Xu
1997 J jnl
Inf. Process. Manag.
Maristella Agosti, James Allan
1997 conf
TREC
Nicholas J. Belkin, Jose Perez Carballo, Colleen Cool, Shin-jeng Lin, Soyeon Park, Soo Young Rieh, Pamela A. Savage-Knepshield, Cynthia A. Sikora, Hong (Iris) Xie, James Allan
1996 J jnl
Inf. Process. Manag.
Gerard Salton, James Allan, Amit Singhal
1996 conf
TREC
James Allan, James P. Callan, W. Bruce Croft, Lisa Ballesteros, John Broglio, Jinxi Xu, Hongming Shu
1996 A* conf
SIGIR
James Allan
1995 J jnl
Inf. Process. Manag.
Chris Buckley, James Allan, Gerard Salton
1995 conf
TREC
James Allan, Lisa Ballesteros, James P. Callan, W. Bruce Croft, Zhihong Lu
1995 A* conf
SIGIR
James Allan
1995 J jnl
Int. J. Hum. Comput. Stud.
Gerard Salton, James Allan
1994 conf
TREC
Chris Buckley, Gerard Salton, James Allan, Amit Singhal
1994 J jnl
Commun. ACM
Gerard Salton, James Allan, Chris Buckley
1994 conf
RIAO
Gerard Salton, James Allan
1994 A* conf
SIGIR
Chris Buckley, Gerard Salton, James Allan
1993 A* conf
SIGIR
Gerard Salton, James Allan, Chris Buckley
1993 conf
TREC
Chris Buckley, James Allan, Gerard Salton
1993 conf
Hypertext
Gerard Salton, James Allan
1993 conf
HLT
Chris Buckley, Gerard Salton, James Allan
1992 conf
TREC
Chris Buckley, Gerard Salton, James Allan
1992 J jnl
Electron. Publ.
Gerard Salton, Chris Buckley, James Allan
tests/unit/test_force_reprocessing.py
← Index tests/unit/test_force_reprocessing.py python
"""
Unit tests for --force flag, --modules selection, --analyzed mode, and --rerun mode.

Tests that:
1. --force bypasses both bulk and per-file dedup checks (feature-extraction AND decompile paths)
2. --modules does NOT implicitly enable force (force must be explicit)
3. Default behavior (no --force) preserves dedup checks
4. --analyzed queries basic_properties and handles decompile dedup filtering
5. --rerun queries disassembled table directly (with --analyzed --decompile)
6. Decompile per-file is_in_code_db respects --force flag
7. YARA scanning skips already-scanned samples by default, rescans all with --force
"""
import os
import sys
import pytest
from unittest.mock import Mock, patch, MagicMock

pytestmark = [pytest.mark.unit]


def _mock_decompiler_module():
    """Install a mock for redb.extractors.decompiler into sys.modules.

    The real DecompileBinja import chain pulls in blake3/binaryninja which
    aren't available in the unit-test environment. This helper injects a
    lightweight mock so that `from redb.extractors.decompiler import DecompileBinja`
    succeeds inside process_binary_file.

    Returns a context-manager-compatible mock DecompileBinja class.
    """
    mock_decompile_binja = Mock()
    mock_instance = Mock()
    mock_instance.export_data.return_value = True
    mock_instance.analysis_results = None
    mock_decompile_binja.return_value.__enter__ = Mock(return_value=mock_instance)
    mock_decompile_binja.return_value.__exit__ = Mock(return_value=False)

    mock_mod = MagicMock()
    mock_mod.DecompileBinja = mock_decompile_binja
    return mock_mod, mock_decompile_binja


# ============================================================================
# CLI Argument Parsing Tests
# ============================================================================

class TestForceCliArgument:
    """Tests for --force CLI argument parsing in start.py."""

    def _parse_args(self, args_list):
        """Helper to parse CLI args without running main()."""
        import argparse

        # Replicate the parser from start.py
        parser = argparse.ArgumentParser()
        input_group = parser.add_mutually_exclusive_group(required=True)
        input_group.add_argument("--path")
        input_group.add_argument("--s3", action="store_true")
        input_group.add_argument("--s3-solo", metavar="S3_KEY")
        input_group.add_argument("--nomad-job", action="store_true")
        input_group.add_argument("--date", metavar="YYYY-MM-DD")
        input_group.add_argument("--range", nargs=2)
        input_group.add_argument("--analyzed", action="store_true")

        parser.add_argument("--repo")
        parser.add_argument("--index_prefix", default="redb")
        parser.add_argument("-d", "--decompile", action="store_true")
        parser.add_argument("-m", "--modules", default="all")
        parser.add_argument("--force", action="store_true")
        parser.add_argument("--dry-run", action="store_true")

        return parser.parse_args(args_list)

    def test_force_flag_default_false(self):
        """--force is False by default."""
        args = self._parse_args(["--path", "/tmp/test", "--repo", "test"])
        assert args.force is False

    def test_force_flag_explicit(self):
        """--force is True when specified."""
        args = self._parse_args(["--path", "/tmp/test", "--repo", "test", "--force"])
        assert args.force is True

    def test_modules_all_no_implicit_force(self):
        """--modules 'all' does not enable force."""
        args = self._parse_args(["--path", "/tmp/test", "--repo", "test", "--modules", "all"])
        assert args.force is False

    def test_specific_modules_no_implicit_force(self):
        """--modules with specific extractors does NOT implicitly enable force."""
        args = self._parse_args([
            "--path", "/tmp/test", "--repo", "test",
            "--modules", "MachOFeaturesExtractor"
        ])
        assert args.force is False

    def test_specific_modules_with_explicit_force(self):
        """--modules with specific extractors requires explicit --force to bypass dedup."""
        args = self._parse_args([
            "--path", "/tmp/test", "--repo", "test",
            "--modules", "BasicPropertiesExtractor,HashExtractor", "--force"
        ])
        assert args.force is True

    def test_force_with_modules_all(self):
        """--force works independently of --modules."""
        args = self._parse_args([
            "--path", "/tmp/test", "--repo", "test",
            "--modules", "all", "--force"
        ])
        assert args.force is True


# ============================================================================
# is_in_db Per-File Dedup Bypass Tests (using module-level patching)
# ============================================================================

class TestPerFileDedupBypass:
    """Tests for the per-file is_in_db check bypass in process_binary_file.

    We patch at the module level to avoid triggering heavy import chains.
    """

    @patch('redb.workers.is_in_db')
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    def test_default_calls_is_in_db(self, mock_settings, mock_magika, mock_hashlib, mock_is_in_db):
        """Without force, is_in_db is called and can skip files."""
        from redb.ingestor import process_binary_file, ImportResult

        # Setup magika
        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "pebin"
        mock_magika.return_value = mock_magika_instance

        # Setup hashlib
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64

        # Setup settings for ClickHouse
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        # is_in_db returns True => file should be skipped
        mock_is_in_db.return_value = True

        logger = Mock()

        with patch('builtins.open', mock_open_binary(b"test data")):
            result, filetype = process_binary_file(
                "/tmp/test.bin", False, "redb", logger,
                selected_modules="all", force=False
            )

        mock_is_in_db.assert_called_once_with("a" * 64, "redb")
        assert result == ImportResult.SKIPPED

    @patch('redb.workers.is_in_db')
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    @patch('redb.workers._is_packed', return_value=None)
    @patch('redb.workers.DIEExtractor')
    @patch('redb.workers.BasicPropertiesExtractor')
    @patch('redb.workers.HashExtractor')
    @patch('redb.workers.CAPAExtractor')
    def test_force_skips_is_in_db(self, mock_capa, mock_hash, mock_bp, mock_die, mock_packed,
                                   mock_settings, mock_magika, mock_hashlib, mock_is_in_db):
        """With force=True, is_in_db is never called.

        Uses filetype 'unknown' to avoid triggering PE/ELF/Mach-O specific
        extractor chains which would need many more mocks.
        """
        from redb.ingestor import process_binary_file

        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "unknown"
        mock_magika.return_value = mock_magika_instance
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        logger = Mock()

        with patch('builtins.open', mock_open_binary(b"test data")):
            try:
                process_binary_file(
                    "/tmp/test.bin", False, "redb", logger,
                    selected_modules="all", force=True
                )
            except Exception:
                pass

        mock_is_in_db.assert_not_called()

    @patch('redb.workers.is_in_db')
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    def test_decompile_mode_skips_is_in_db(self, mock_settings, mock_magika, mock_hashlib, mock_is_in_db):
        """In decompile mode, is_in_db is never called (existing behavior)."""
        from redb.ingestor import process_binary_file

        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "pebin"
        mock_magika.return_value = mock_magika_instance
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        logger = Mock()

        with patch('builtins.open', mock_open_binary(b"test data")):
            try:
                process_binary_file(
                    "/tmp/test.bin", True, "redb", logger,
                    selected_modules="all", force=False
                )
            except Exception:
                pass

        mock_is_in_db.assert_not_called()


# ============================================================================
# Bulk Dedup Bypass Tests
# ============================================================================

class TestBulkDedupBypass:
    """Tests for the bulk is_in_db_bulk check bypass in fetch functions."""

    @patch('redb.queries.is_in_db_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.get_db_catalog_connection')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_by_repo_default_calls_bulk_check(
        self, mock_db_conn, mock_formats, mock_gen_key, mock_bulk
    ):
        """Without force, fetch_s3_objects_by_repository calls is_in_db_bulk."""
        from redb.ingestor import fetch_s3_objects_by_repository

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "pebin", None)
        ]
        mock_db_conn.return_value = mock_client
        mock_bulk.return_value = set()

        fetch_s3_objects_by_repository("test_repo", "redb", False, force=False)

        mock_bulk.assert_called_once()

    @patch('redb.queries.is_in_db_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.get_db_catalog_connection')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_by_repo_force_skips_bulk_check(
        self, mock_db_conn, mock_formats, mock_gen_key, mock_bulk
    ):
        """With force=True, fetch_s3_objects_by_repository skips is_in_db_bulk."""
        from redb.ingestor import fetch_s3_objects_by_repository

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "pebin", None)
        ]
        mock_db_conn.return_value = mock_client

        result = fetch_s3_objects_by_repository("test_repo", "redb", False, force=True)

        mock_bulk.assert_not_called()
        assert len(result) == 1  # All rows kept, none filtered

    @patch('redb.queries.is_in_db_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.get_db_catalog_connection')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_by_repo_force_preserves_all_rows(
        self, mock_db_conn, mock_formats, mock_gen_key, mock_bulk
    ):
        """With force=True, all rows from the query are returned unfiltered."""
        from redb.ingestor import fetch_s3_objects_by_repository

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("hash1" + "0" * 59, "pebin", None),
            ("hash2" + "0" * 59, "pebin", None),
            ("hash3" + "0" * 59, "pebin", None),
        ]
        mock_db_conn.return_value = mock_client

        result = fetch_s3_objects_by_repository("test_repo", "redb", False, force=True)

        assert len(result) == 3

    @patch('redb.queries.is_in_db_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.get_db_catalog_connection')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_by_date_default_calls_bulk_check(
        self, mock_db_conn, mock_formats, mock_gen_key, mock_bulk
    ):
        """Without force, fetch_s3_objects_by_date_range calls is_in_db_bulk."""
        from redb.ingestor import fetch_s3_objects_by_date_range

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "pebin", None)
        ]
        mock_db_conn.return_value = mock_client
        mock_bulk.return_value = set()

        fetch_s3_objects_by_date_range(
            "redb", False, "2025-01-01", "2025-01-02", force=False
        )

        mock_bulk.assert_called_once()

    @patch('redb.queries.is_in_db_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.get_db_catalog_connection')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_by_date_force_skips_bulk_check(
        self, mock_db_conn, mock_formats, mock_gen_key, mock_bulk
    ):
        """With force=True, fetch_s3_objects_by_date_range skips is_in_db_bulk."""
        from redb.ingestor import fetch_s3_objects_by_date_range

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "pebin", None)
        ]
        mock_db_conn.return_value = mock_client

        result = fetch_s3_objects_by_date_range(
            "redb", False, "2025-01-01", "2025-01-02", force=True
        )

        mock_bulk.assert_not_called()
        assert len(result) == 1


# ============================================================================
# Ingestor Force Parameter Tests
# ============================================================================

class TestIngestorForceParam:
    """Tests for force parameter in Ingestor class."""

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_default_force_false(self, mock_open, mock_datetime, mock_mp):
        """Ingestor defaults force to False."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(path="/tmp/test", repository="test", index_prefix="redb")
        assert ingestor.force is False

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_force_true(self, mock_open, mock_datetime, mock_mp):
        """Ingestor stores force=True when passed."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(path="/tmp/test", repository="test", index_prefix="redb", force=True)
        assert ingestor.force is True


# ============================================================================
# Function Chain Threading Tests
# ============================================================================

class TestForceThreading:
    """Tests that force parameter is correctly threaded through the call chain."""

    @patch('redb.workers.process_binary_file')
    @patch('redb.workers.is_binary_file', return_value=True)
    def test_process_file_internal_passes_force_true(self, mock_is_binary, mock_process):
        """_process_file_internal passes force=True to process_binary_file."""
        from redb.ingestor import _process_file_internal

        mock_process.return_value = (Mock(), "pebin")
        logger = Mock()

        _process_file_internal("/tmp/test.bin", False, "redb", logger, force=True)

        mock_process.assert_called_once()
        call_args = mock_process.call_args
        assert call_args[0][-1] is True

    @patch('redb.workers.process_binary_file')
    @patch('redb.workers.is_binary_file', return_value=True)
    def test_process_file_internal_default_force_false(self, mock_is_binary, mock_process):
        """_process_file_internal defaults force to False."""
        from redb.ingestor import _process_file_internal

        mock_process.return_value = (Mock(), "pebin")
        logger = Mock()

        _process_file_internal("/tmp/test.bin", False, "redb", logger)

        call_args = mock_process.call_args
        assert call_args[0][-1] is False

    @patch('redb.workers._process_file_internal')
    def test_process_file_passes_force(self, mock_internal):
        """process_file passes force to _process_file_internal."""
        from redb.workers import process_file

        mock_internal.return_value = (Mock(), "pebin")

        with patch('redb.workers.setup_direct_logger', return_value=Mock()):
            process_file("/tmp/test.bin", False, "redb", "/tmp/log", 1, 1, force=True)

        mock_internal.assert_called_once()
        call_args = mock_internal.call_args
        assert call_args[0][-1] is True

    @patch('redb.workers._process_file_internal')
    def test_process_s3_file_passes_force(self, mock_internal):
        """process_s3_file passes force to _process_file_internal."""
        from redb.workers import process_s3_file

        mock_internal.return_value = (Mock(), "pebin")

        with patch('redb.workers.setup_direct_logger', return_value=Mock()), \
             patch('redb.workers.download_s3_object', return_value="/tmp/downloaded.bin"):
            process_s3_file(
                "bucket", "key", "/tmp", False, "redb", "/tmp/log", 1, 1,
                force=True
            )

        mock_internal.assert_called_once()
        call_args = mock_internal.call_args
        assert call_args[0][-1] is True

    @patch('redb.workers.process_binary_file')
    def test_process_zip_passes_force(self, mock_process):
        """process_zip_file passes force to process_binary_file."""
        from redb.workers import process_zip_file

        mock_process.return_value = (Mock(), "pebin")
        logger = Mock()

        import tempfile
        import zipfile

        # Create a real zip file with a dummy binary inside
        with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
            tmp_path = tmp.name

        try:
            with zipfile.ZipFile(tmp_path, 'w') as zf:
                zf.writestr("test.bin", b"\x00" * 100)

            process_zip_file(tmp_path, False, "redb", logger, "all", force=True)

            mock_process.assert_called_once()
            call_args = mock_process.call_args
            assert call_args[0][-1] is True
        finally:
            os.unlink(tmp_path)

    @patch('redb.workers.process_binary_file')
    def test_process_7zip_passes_force(self, mock_process):
        """process_7zip_file passes force to process_binary_file."""
        from redb.workers import process_7zip_file

        mock_process.return_value = (Mock(), "pebin")
        logger = Mock()

        with patch('redb.workers.py7zr') as mock_py7zr:
            mock_zf = Mock()
            mock_py7zr.SevenZipFile.return_value.__enter__ = Mock(return_value=mock_zf)
            mock_py7zr.SevenZipFile.return_value.__exit__ = Mock(return_value=False)

            import tempfile

            def fake_extractall(path):
                dest = os.path.join(path, "test.bin")
                with open(dest, "wb") as f:
                    f.write(b"\x00" * 100)

            mock_zf.extractall = fake_extractall

            process_7zip_file("/tmp/test.7z", False, "redb", logger, "all", force=True)

            if mock_process.called:
                call_args = mock_process.call_args
                assert call_args[0][-1] is True


# ============================================================================
# --analyzed CLI Argument Tests
# ============================================================================

class TestAnalyzedCliArgument:
    """Tests for --analyzed CLI argument parsing in start.py."""

    def _parse_args(self, args_list):
        """Helper to parse CLI args without running main()."""
        import argparse

        parser = argparse.ArgumentParser()
        input_group = parser.add_mutually_exclusive_group(required=True)
        input_group.add_argument("--path")
        input_group.add_argument("--s3", action="store_true")
        input_group.add_argument("--s3-solo", metavar="S3_KEY")
        input_group.add_argument("--nomad-job", action="store_true")
        input_group.add_argument("--date", metavar="YYYY-MM-DD")
        input_group.add_argument("--range", nargs=2)
        input_group.add_argument("--analyzed", action="store_true")

        parser.add_argument("--repo")
        parser.add_argument("--index_prefix", default="redb")
        parser.add_argument("-d", "--decompile", action="store_true")
        parser.add_argument("-m", "--modules", default="all")
        parser.add_argument("--force", action="store_true")
        parser.add_argument("--dry-run", action="store_true")
        parser.add_argument("--magika")

        return parser.parse_args(args_list)

    def test_analyzed_flag_default_false(self):
        """--analyzed is False by default."""
        args = self._parse_args(["--path", "/tmp/test", "--repo", "test"])
        assert args.analyzed is False

    def test_analyzed_flag_explicit(self):
        """--analyzed is True when specified."""
        args = self._parse_args(["--analyzed"])
        assert args.analyzed is True

    def test_analyzed_mutually_exclusive_with_path(self):
        """--analyzed and --path cannot be used together."""
        import argparse
        with pytest.raises(SystemExit):
            self._parse_args(["--analyzed", "--path", "/tmp/test"])

    def test_analyzed_mutually_exclusive_with_s3(self):
        """--analyzed and --s3 cannot be used together."""
        import argparse
        with pytest.raises(SystemExit):
            self._parse_args(["--analyzed", "--s3"])

    def test_analyzed_with_decompile(self):
        """--analyzed can be combined with --decompile."""
        args = self._parse_args(["--analyzed", "--decompile"])
        assert args.analyzed is True
        assert args.decompile is True

    def test_analyzed_with_modules(self):
        """--analyzed can be combined with --modules."""
        args = self._parse_args(["--analyzed", "--modules", "HashExtractor"])
        assert args.analyzed is True
        assert args.modules == "HashExtractor"

    def test_analyzed_with_magika(self):
        """--analyzed can be combined with --magika."""
        args = self._parse_args(["--analyzed", "--magika", "elf"])
        assert args.analyzed is True
        assert args.magika == "elf"

    def test_analyzed_with_force(self):
        """--analyzed can be combined with --force."""
        args = self._parse_args(["--analyzed", "--force"])
        assert args.analyzed is True
        assert args.force is True

    def test_analyzed_no_repo_required(self):
        """--analyzed does not require --repo."""
        args = self._parse_args(["--analyzed"])
        assert args.analyzed is True
        assert args.repo is None


# ============================================================================
# fetch_analyzed_samples Tests
# ============================================================================

class TestFetchAnalyzedSamples:
    """Tests for fetch_analyzed_samples() in queries.py."""

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_returns_analyzed_samples(self, mock_settings, mock_formats, mock_gen_key):
        """fetch_analyzed_samples returns samples from basic_properties."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "pebin"),
            ("def456" + "0" * 58, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=False)

        assert len(result) == 2
        assert all(r['s3_bucket'] == 'test-bucket' for r in result)
        assert all(r['s3_key'] == 'ab/cd/abcd.zip' for r in result)

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_queries_basic_properties_table(self, mock_settings, mock_formats, mock_gen_key):
        """fetch_analyzed_samples queries the correct table."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = []
        mock_settings.create_clickhouse_client.return_value = mock_client

        fetch_analyzed_samples("myprefix", decompile=False)

        query_arg = mock_client.query.call_args[0][0]
        assert "myprefix_basic_properties" in query_arg
        assert "FINAL" in query_arg

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_magika_filter_applied(self, mock_settings, mock_gen_key):
        """fetch_analyzed_samples applies magika_filter when provided."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "elf"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=False, magika_filter="elf")

        query_arg = mock_client.query.call_args[0][0]
        assert "filetype_magika = %(magika)s" in query_arg
        params = mock_client.query.call_args[1].get('parameters') or mock_client.query.call_args[0][1] if len(mock_client.query.call_args[0]) > 1 else mock_client.query.call_args[1].get('parameters', {})
        assert params.get("magika") == "elf"

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_decompile_mode_filters_already_decompiled(self, mock_settings, mock_formats, mock_gen_key):
        """In decompile mode, already-decompiled samples are filtered out."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        # First query: basic_properties returns 3 samples
        # Second query: decompiled table returns 1 already-decompiled
        mock_client.query.side_effect = [
            Mock(result_rows=[
                ("hash1" + "0" * 59, "pebin"),
                ("hash2" + "0" * 59, "pebin"),
                ("hash3" + "0" * 59, "pebin"),
            ]),
            Mock(result_rows=[
                ("hash2" + "0" * 59,),  # already decompiled
            ]),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=True)

        assert len(result) == 2
        result_hashes = {r['sha256'] for r in result}
        assert "hash2" + "0" * 59 not in result_hashes

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_decompile_force_skips_decompile_check(self, mock_settings, mock_formats, mock_gen_key):
        """With force=True in decompile mode, decompile check is skipped."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("hash1" + "0" * 59, "pebin"),
            ("hash2" + "0" * 59, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=True, force=True)

        # Should only query once (basic_properties), no decompile check
        assert mock_client.query.call_count == 1
        assert len(result) == 2

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_analysis_mode_no_decompile_filtering(self, mock_settings, mock_formats, mock_gen_key):
        """In analysis mode (decompile=False), no decompile filtering is done."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("hash1" + "0" * 59, "pebin"),
            ("hash2" + "0" * 59, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=False)

        # Only one query (basic_properties), no decompile table check
        assert mock_client.query.call_count == 1
        assert len(result) == 2

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    def test_missing_s3_bucket_returns_empty(self, mock_settings, mock_formats, mock_gen_key):
        """Returns empty list when S3_BUCKET env var is not set."""
        from redb.queries import fetch_analyzed_samples

        # Ensure S3_BUCKET is not set
        with patch.dict(os.environ, {}, clear=True):
            result = fetch_analyzed_samples("redb", decompile=False)

        assert result == []

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_empty_basic_properties_returns_empty(self, mock_settings, mock_formats, mock_gen_key):
        """Returns empty list when no analyzed samples exist."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = []
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=False)

        assert result == []

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_handles_bytes_sha256(self, mock_settings, mock_formats, mock_gen_key):
        """Handles bytes-encoded SHA256 from ClickHouse."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        hash_str = "abc123" + "0" * 58
        mock_client.query.return_value.result_rows = [
            (hash_str.encode('utf-8'), "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=False)

        assert len(result) == 1
        assert result[0]['sha256'] == hash_str

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_exception_returns_empty(self, mock_settings, mock_formats, mock_gen_key):
        """Returns empty list on database error."""
        from redb.queries import fetch_analyzed_samples

        mock_settings.create_clickhouse_client.side_effect = Exception("Connection failed")

        result = fetch_analyzed_samples("redb", decompile=False)

        assert result == []

    @patch('redb.queries._check_yara_matches_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_yara_scan_skips_decompile_filtering(self, mock_settings, mock_formats, mock_gen_key, mock_yara_check):
        """With yara_scan=True, decompile filtering is skipped (YARA check runs instead)."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("hash1" + "0" * 59, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client
        mock_yara_check.return_value = set()  # No already-scanned samples

        result = fetch_analyzed_samples("redb", decompile=True, yara_scan=True)

        # basic_properties queried once, no decompile table check
        assert mock_client.query.call_count == 1
        # YARA check was used instead of decompile check
        mock_yara_check.assert_called_once()
        assert len(result) == 1

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket", "CLICKHOUSE_CODE_PREFIX": "custom_code"})
    def test_decompile_uses_code_prefix_env(self, mock_settings, mock_formats, mock_gen_key):
        """Decompile filtering uses CLICKHOUSE_CODE_PREFIX env var."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.side_effect = [
            Mock(result_rows=[("hash1" + "0" * 59, "pebin")]),
            Mock(result_rows=[]),  # No disassembled samples
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        fetch_analyzed_samples("redb", decompile=True)

        # Second query should reference custom_code prefix with disassembled table
        second_query = mock_client.query.call_args_list[1][0][0]
        assert "custom_code_disassembled_functions_references" in second_query


# ============================================================================
# Ingestor Analyzed Parameter Tests
# ============================================================================

class TestIngestorAnalyzedParam:
    """Tests for analyzed parameter in Ingestor class."""

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_default_analyzed_false(self, mock_open, mock_datetime, mock_mp):
        """Ingestor defaults analyzed to False."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(path="/tmp/test", repository="test", index_prefix="redb")
        assert ingestor.analyzed is False

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_analyzed_true(self, mock_open, mock_datetime, mock_mp):
        """Ingestor stores analyzed=True when passed."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(
            path=None, repository="test", index_prefix="redb",
            s3_mode=True, analyzed=True
        )
        assert ingestor.analyzed is True

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_analyzed_with_decompile(self, mock_open, mock_datetime, mock_mp):
        """Ingestor stores both analyzed and decompile flags."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(
            path=None, repository="test", index_prefix="redb",
            s3_mode=True, analyzed=True, decompile=True
        )
        assert ingestor.analyzed is True
        assert ingestor.decompile is True


# ============================================================================
# Re-export Test for fetch_analyzed_samples
# ============================================================================

class TestAnalyzedReExport:
    """Tests that fetch_analyzed_samples is re-exported from redb.ingestor."""

    def test_importable_from_ingestor(self):
        """fetch_analyzed_samples can be imported from redb.ingestor."""
        from redb.ingestor import fetch_analyzed_samples
        assert callable(fetch_analyzed_samples)


# ============================================================================
# Decompile-Path Per-File Dedup (is_in_code_db) Tests
# ============================================================================

class TestDecompilePerFileDedupBypass:
    """Tests for the per-file is_in_code_db check in the decompile path.

    Verifies that --force correctly bypasses the is_in_code_db check
    for both normal binaries and FAT Mach-O slices.

    Note: DecompileBinja requires blake3/binaryninja which aren't available
    in the test environment. We use two strategies:
    - For "no force" tests: is_in_code_db returns True so the code never
      reaches the DecompileBinja import.
    - For "force" tests: we mock the import mechanism to prevent the real
      import from happening.
    """

    @patch('redb.workers.is_in_code_db')
    @patch('redb.workers.is_in_db')
    @patch('redb.workers._is_packed', return_value=None)
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    def test_decompile_no_force_calls_is_in_code_db(
        self, mock_settings, mock_magika, mock_hashlib,
        mock_packed, mock_is_in_db, mock_is_in_code_db
    ):
        """In decompile mode without --force, is_in_code_db is called and can skip files."""
        from redb.ingestor import process_binary_file, ImportResult

        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "pebin"
        mock_magika.return_value = mock_magika_instance
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        # is_in_code_db returns True => file should be skipped
        mock_is_in_code_db.return_value = True

        logger = Mock()

        mock_mod, _ = _mock_decompiler_module()
        with patch.dict(sys.modules, {"redb.extractors.decompiler": mock_mod}):
            with patch('builtins.open', mock_open_binary(b"test data")):
                result, filetype = process_binary_file(
                    "/tmp/test.bin", True, "redb", logger,
                    selected_modules="all", force=False
                )

        mock_is_in_code_db.assert_called_once_with("a" * 64, filetype="pebin")
        assert result == ImportResult.SKIPPED

    @patch('redb.workers.is_in_code_db')
    @patch('redb.workers.is_in_db')
    @patch('redb.workers._is_packed', return_value=None)
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    def test_decompile_no_force_not_in_code_db_proceeds(
        self, mock_settings, mock_magika, mock_hashlib,
        mock_packed, mock_is_in_db, mock_is_in_code_db
    ):
        """In decompile mode without --force, files NOT in code_db proceed to decompile."""
        from redb.ingestor import process_binary_file

        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "pebin"
        mock_magika.return_value = mock_magika_instance
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        # is_in_code_db returns False => should proceed to decompile
        mock_is_in_code_db.return_value = False

        logger = Mock()

        mock_mod, mock_decompiler = _mock_decompiler_module()
        with patch.dict(sys.modules, {"redb.extractors.decompiler": mock_mod}):
            with patch('builtins.open', mock_open_binary(b"test data")):
                try:
                    process_binary_file(
                        "/tmp/test.bin", True, "redb", logger,
                        selected_modules="all", force=False
                    )
                except Exception:
                    pass

        mock_is_in_code_db.assert_called_once_with("a" * 64, filetype="pebin")

    @patch('redb.workers.is_in_code_db')
    @patch('redb.workers.is_in_db')
    @patch('redb.workers._is_packed', return_value=None)
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    def test_decompile_force_skips_is_in_code_db(
        self, mock_settings, mock_magika, mock_hashlib,
        mock_packed, mock_is_in_db, mock_is_in_code_db
    ):
        """In decompile mode with --force, is_in_code_db is never called."""
        from redb.ingestor import process_binary_file

        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "pebin"
        mock_magika.return_value = mock_magika_instance
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        logger = Mock()

        mock_mod, _ = _mock_decompiler_module()
        with patch.dict(sys.modules, {"redb.extractors.decompiler": mock_mod}):
            with patch('builtins.open', mock_open_binary(b"test data")):
                try:
                    process_binary_file(
                        "/tmp/test.bin", True, "redb", logger,
                        selected_modules="all", force=True
                    )
                except Exception:
                    pass

        # The key assertion: is_in_code_db was never called
        mock_is_in_code_db.assert_not_called()

    @patch('redb.workers.is_in_code_db')
    @patch('redb.workers.is_in_db')
    @patch('redb.workers._is_packed', return_value=None)
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    def test_decompile_packed_skips_is_in_code_db(
        self, mock_settings, mock_magika, mock_hashlib,
        mock_packed, mock_is_in_db, mock_is_in_code_db
    ):
        """Packed files skip decompile entirely, so is_in_code_db is never called."""
        from redb.ingestor import process_binary_file

        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "pebin"
        mock_magika.return_value = mock_magika_instance
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        # _is_packed returns a truthy value (packer name)
        mock_packed.return_value = "UPX"

        logger = Mock()

        mock_mod, _ = _mock_decompiler_module()
        with patch.dict(sys.modules, {"redb.extractors.decompiler": mock_mod}):
            with patch('builtins.open', mock_open_binary(b"test data")):
                try:
                    process_binary_file(
                        "/tmp/test.bin", True, "redb", logger,
                        selected_modules="all", force=False
                    )
                except Exception:
                    pass

        mock_is_in_code_db.assert_not_called()

    @patch('redb.workers.is_in_code_db')
    @patch('redb.workers.is_in_db')
    @patch('redb.workers.hashlib')
    @patch('redb.workers.Magika')
    @patch('redb.workers.settings')
    def test_non_decompile_mode_never_calls_is_in_code_db(
        self, mock_settings, mock_magika, mock_hashlib,
        mock_is_in_db, mock_is_in_code_db
    ):
        """In feature-extraction mode (no decompile), is_in_code_db is never called."""
        from redb.ingestor import process_binary_file, ImportResult

        mock_magika_instance = Mock()
        mock_magika_instance.identify_bytes.return_value.output.label = "pebin"
        mock_magika.return_value = mock_magika_instance
        mock_hashlib.sha256.return_value.hexdigest.return_value = "a" * 64
        mock_settings.CLICKHOUSE_HOST = "localhost"
        mock_settings.get_clickhouse_client.return_value = Mock()

        # is_in_db returns True => file skipped by feature-extraction dedup
        mock_is_in_db.return_value = True

        logger = Mock()

        with patch('builtins.open', mock_open_binary(b"test data")):
            result, filetype = process_binary_file(
                "/tmp/test.bin", False, "redb", logger,
                selected_modules="all", force=False
            )

        assert result == ImportResult.SKIPPED
        mock_is_in_db.assert_called_once()
        mock_is_in_code_db.assert_not_called()


# ============================================================================
# --rerun CLI Argument Tests
# ============================================================================

class TestRerunCliArgument:
    """Tests for --rerun CLI argument parsing and validation."""

    def _parse_args(self, args_list):
        """Helper to parse CLI args."""
        import argparse

        parser = argparse.ArgumentParser()
        input_group = parser.add_mutually_exclusive_group(required=True)
        input_group.add_argument("--path")
        input_group.add_argument("--s3", action="store_true")
        input_group.add_argument("--s3-solo", metavar="S3_KEY")
        input_group.add_argument("--nomad-job", action="store_true")
        input_group.add_argument("--date", metavar="YYYY-MM-DD")
        input_group.add_argument("--range", nargs=2)
        input_group.add_argument("--analyzed", action="store_true")

        parser.add_argument("--repo")
        parser.add_argument("--index_prefix", default="redb")
        parser.add_argument("-d", "--decompile", action="store_true")
        parser.add_argument("-m", "--modules", default="all")
        parser.add_argument("--force", action="store_true")
        parser.add_argument("--rerun", action="store_true")
        parser.add_argument("--dry-run", action="store_true")

        return parser.parse_args(args_list)

    def test_rerun_flag_default_false(self):
        """--rerun is False by default."""
        args = self._parse_args(["--analyzed", "-d"])
        assert args.rerun is False

    def test_rerun_flag_explicit(self):
        """--rerun is True when specified."""
        args = self._parse_args(["--analyzed", "-d", "--rerun"])
        assert args.rerun is True

    def test_rerun_with_analyzed_and_decompile(self):
        """--rerun can be combined with --analyzed and --decompile."""
        args = self._parse_args(["--analyzed", "-d", "--rerun"])
        assert args.rerun is True
        assert args.analyzed is True
        assert args.decompile is True


# ============================================================================
# --rerun fetch_analyzed_samples Tests
# ============================================================================

class TestRerunFetchAnalyzedSamples:
    """Tests for --rerun behavior in fetch_analyzed_samples."""

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_rerun_queries_disassembled_table(self, mock_settings, mock_formats, mock_gen_key):
        """Rerun mode queries disassembled_functions_references with JOIN to basic_properties."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("hash1" + "0" * 59, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=True, rerun=True)

        # Should only make one query (the JOIN query)
        assert mock_client.query.call_count == 1
        query = mock_client.query.call_args_list[0][0][0]
        assert "code_binja_disassembled_functions_references" in query
        assert "redb_basic_properties" in query
        assert "INNER JOIN" in query
        assert len(result) == 1

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_rerun_returns_only_disassembled_samples(self, mock_settings, mock_formats, mock_gen_key):
        """Rerun mode returns only samples present in disassembled table."""
        from redb.queries import fetch_analyzed_samples

        hash1 = "hash1" + "0" * 59
        mock_client = Mock()
        mock_client.query.return_value.result_rows = [(hash1, "pebin")]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=True, rerun=True)

        assert len(result) == 1
        assert result[0]['sha256'] == hash1
        assert result[0]['s3_bucket'] == "test-bucket"

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_rerun_with_magika_filter(self, mock_settings, mock_formats, mock_gen_key):
        """Rerun mode applies magika filter in the JOIN query."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("hash1" + "0" * 59, "elf"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=True, rerun=True, magika_filter="elf")

        query = mock_client.query.call_args_list[0][0][0]
        assert "bp.filetype_magika" in query
        params = mock_client.query.call_args_list[0][1].get("parameters", {})
        assert params.get("magika") == "elf"
        assert len(result) == 1

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket", "CLICKHOUSE_CODE_PREFIX": "custom_code"})
    def test_rerun_uses_code_prefix_env(self, mock_settings, mock_formats, mock_gen_key):
        """Rerun mode uses CLICKHOUSE_CODE_PREFIX env var."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = []
        mock_settings.create_clickhouse_client.return_value = mock_client

        fetch_analyzed_samples("redb", decompile=True, rerun=True)

        query = mock_client.query.call_args_list[0][0][0]
        assert "custom_code_disassembled_functions_references" in query

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_rerun_empty_disassembled_table(self, mock_settings, mock_formats, mock_gen_key):
        """Rerun mode returns empty list when no disassembled samples exist."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = []
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=True, rerun=True)

        assert result == []

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    def test_rerun_missing_s3_bucket(self, mock_settings, mock_formats, mock_gen_key):
        """Rerun mode returns empty list when S3_BUCKET is not set."""
        from redb.queries import fetch_analyzed_samples

        with patch.dict(os.environ, {}, clear=True):
            result = fetch_analyzed_samples("redb", decompile=True, rerun=True)

        assert result == []

    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_rerun_handles_bytes_sha256(self, mock_settings, mock_formats, mock_gen_key):
        """Rerun mode handles bytes-encoded SHA256 from ClickHouse."""
        from redb.queries import fetch_analyzed_samples

        hash_str = "abc123" + "0" * 58
        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            (hash_str.encode('utf-8'), "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=True, rerun=True)

        assert len(result) == 1
        assert result[0]['sha256'] == hash_str


# ============================================================================
# --rerun Ingestor Parameter Tests
# ============================================================================

class TestIngestorRerunParam:
    """Tests for rerun parameter in Ingestor class."""

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_default_rerun_false(self, mock_open, mock_datetime, mock_mp):
        """Ingestor defaults rerun to False."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(path="/tmp/test", repository="test", index_prefix="redb")
        assert ingestor.rerun is False

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_rerun_true(self, mock_open, mock_datetime, mock_mp):
        """Ingestor stores rerun=True when passed."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(
            path=None, repository="test", index_prefix="redb",
            s3_mode=True, analyzed=True, decompile=True, rerun=True
        )
        assert ingestor.rerun is True

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def test_ingestor_rerun_with_decompile_modules(self, mock_open, mock_datetime, mock_mp):
        """Ingestor stores rerun alongside decompile_modules."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        ingestor = Ingestor(
            path=None, repository="test", index_prefix="redb",
            s3_mode=True, analyzed=True, decompile=True, rerun=True,
            decompile_modules={"cfg"}
        )
        assert ingestor.rerun is True
        assert ingestor.decompile_modules == {"cfg"}


# ============================================================================
# YARA --force Dedup Tests
# ============================================================================

class TestYaraForceDedup:
    """Tests for --force behavior with YARA scanning.

    Default (no --force): skip samples already in yara_matches.
    With --force: scan everything (rescan all samples).
    """

    # --- is_in_db_bulk YARA path ---

    @patch('redb.queries.settings')
    def test_is_in_db_bulk_yara_returns_already_scanned(self, mock_settings):
        """In YARA mode, is_in_db_bulk queries yara_matches and returns already-scanned hashes."""
        from redb.queries import is_in_db_bulk

        mock_client = Mock()
        # hex(sha256) returns uppercase by convention from ClickHouse
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58,),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = is_in_db_bulk(
            ["abc123" + "0" * 58, "def456" + "0" * 58],
            "redb", decompile=False, yara_scan=True
        )

        # Should have queried yara_matches
        query = mock_client.query.call_args[0][0]
        assert "yara_matches" in query
        assert "unhex(" in query
        # Should return the already-scanned hash
        assert "abc123" + "0" * 58 in result
        assert "def456" + "0" * 58 not in result

    @patch('redb.queries.settings')
    def test_is_in_db_bulk_yara_empty_table(self, mock_settings):
        """In YARA mode with empty yara_matches, no samples are skipped."""
        from redb.queries import is_in_db_bulk

        mock_client = Mock()
        mock_client.query.return_value.result_rows = []
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = is_in_db_bulk(
            ["abc123" + "0" * 58],
            "redb", decompile=False, yara_scan=True
        )

        assert len(result) == 0

    @patch('redb.queries.settings')
    def test_is_in_db_bulk_yara_handles_bytes_response(self, mock_settings):
        """YARA dedup correctly handles bytes-encoded hex response from ClickHouse."""
        from redb.queries import is_in_db_bulk

        hash_hex = "abc123" + "0" * 58
        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            (hash_hex.upper().encode('utf-8'),),  # bytes, uppercase
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = is_in_db_bulk(
            [hash_hex],
            "redb", decompile=False, yara_scan=True
        )

        # Should normalize to lowercase
        assert hash_hex in result

    @patch('redb.queries.settings')
    def test_is_in_db_bulk_yara_db_error_raises(self, mock_settings):
        """On DB error, YARA dedup raises RuntimeError to prevent reprocessing all samples."""
        from redb.queries import is_in_db_bulk
        import pytest

        mock_settings.create_clickhouse_client.side_effect = Exception("Connection failed")

        with pytest.raises(RuntimeError, match="YARA dedup query failed"):
            is_in_db_bulk(
                ["abc123" + "0" * 58],
                "redb", decompile=False, yara_scan=True
            )

    def test_is_in_db_bulk_yara_empty_list(self):
        """is_in_db_bulk with empty list returns empty set."""
        from redb.queries import is_in_db_bulk

        result = is_in_db_bulk([], "redb", decompile=False, yara_scan=True)
        assert result == set()

    # --- fetch_s3_objects_by_repository YARA+force ---

    @patch('redb.queries.is_in_db_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.get_db_catalog_connection')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_by_repo_yara_default_calls_bulk_check(
        self, mock_db_conn, mock_formats, mock_gen_key, mock_bulk
    ):
        """Without --force, YARA fetch calls is_in_db_bulk to skip already-scanned."""
        from redb.ingestor import fetch_s3_objects_by_repository

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "pebin", None),
            ("def456" + "0" * 58, "pebin", None),
        ]
        mock_db_conn.return_value = mock_client
        mock_bulk.return_value = {"abc123" + "0" * 58}  # One already scanned

        result = fetch_s3_objects_by_repository(
            "test_repo", "redb", False, yara_scan=True, force=False
        )

        mock_bulk.assert_called_once()
        # The yara_scan=True should be passed through
        assert mock_bulk.call_args[0][3] is True  # yara_scan arg
        # One sample should be filtered out
        assert len(result) == 1

    @patch('redb.queries.is_in_db_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.get_db_catalog_connection')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_by_repo_yara_force_skips_bulk_check(
        self, mock_db_conn, mock_formats, mock_gen_key, mock_bulk
    ):
        """With --force, YARA fetch skips is_in_db_bulk entirely."""
        from redb.ingestor import fetch_s3_objects_by_repository

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("abc123" + "0" * 58, "pebin", None),
            ("def456" + "0" * 58, "pebin", None),
        ]
        mock_db_conn.return_value = mock_client

        result = fetch_s3_objects_by_repository(
            "test_repo", "redb", False, yara_scan=True, force=True
        )

        mock_bulk.assert_not_called()
        assert len(result) == 2

    # --- fetch_analyzed_samples YARA+force ---

    @patch('redb.queries._check_yara_matches_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_analyzed_yara_default_filters_already_scanned(
        self, mock_settings, mock_formats, mock_gen_key, mock_yara_check
    ):
        """Without --force, fetch_analyzed_samples filters already YARA-scanned samples."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        hash1 = "hash1" + "0" * 59
        hash2 = "hash2" + "0" * 59
        hash3 = "hash3" + "0" * 59
        mock_client.query.return_value.result_rows = [
            (hash1, "pebin"),
            (hash2, "pebin"),
            (hash3, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client
        mock_yara_check.return_value = {hash2}  # hash2 already scanned

        result = fetch_analyzed_samples("redb", decompile=False, yara_scan=True, force=False)

        mock_yara_check.assert_called_once()
        assert len(result) == 2
        result_hashes = {r['sha256'] for r in result}
        assert hash2 not in result_hashes

    @patch('redb.queries._check_yara_matches_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_analyzed_yara_force_skips_yara_check(
        self, mock_settings, mock_formats, mock_gen_key, mock_yara_check
    ):
        """With --force, fetch_analyzed_samples skips YARA dedup check."""
        from redb.queries import fetch_analyzed_samples

        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            ("hash1" + "0" * 59, "pebin"),
            ("hash2" + "0" * 59, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client

        result = fetch_analyzed_samples("redb", decompile=False, yara_scan=True, force=True)

        mock_yara_check.assert_not_called()
        assert len(result) == 2

    @patch('redb.queries._check_yara_matches_bulk')
    @patch('redb.queries.generate_s3_key_from_hash', return_value="ab/cd/abcd.zip")
    @patch('redb.queries.get_supported_formats', return_value=["pebin"])
    @patch('redb.queries.settings')
    @patch.dict(os.environ, {"S3_BUCKET": "test-bucket"})
    def test_fetch_analyzed_yara_all_already_scanned(
        self, mock_settings, mock_formats, mock_gen_key, mock_yara_check
    ):
        """When all samples already scanned, returns empty list."""
        from redb.queries import fetch_analyzed_samples

        hash1 = "hash1" + "0" * 59
        mock_client = Mock()
        mock_client.query.return_value.result_rows = [
            (hash1, "pebin"),
        ]
        mock_settings.create_clickhouse_client.return_value = mock_client
        mock_yara_check.return_value = {hash1}

        result = fetch_analyzed_samples("redb", decompile=False, yara_scan=True, force=False)

        assert len(result) == 0

    # --- _check_yara_matches_bulk direct tests ---

    @patch('redb.queries.settings')
    def test_check_yara_matches_bulk_builds_unhex_query(self, mock_settings):
        """_check_yara_matches_bulk uses unhex() for binary sha256 comparison."""
        from redb.queries import _check_yara_matches_bulk

        mock_client = Mock()
        mock_client.query.return_value.result_rows = []
        mock_settings.create_clickhouse_client.return_value = mock_client

        hash1 = "aabb" + "0" * 60
        _check_yara_matches_bulk([hash1])

        query = mock_client.query.call_args[0][0]
        assert f"unhex('{hash1}')" in query
        assert "hex(sha256)" in query
        assert "yara_matches" in query

    @patch('redb.queries.settings')
    def test_check_yara_matches_bulk_batches_large_lists(self, mock_settings):
        """_check_yara_matches_bulk batches queries for large hash lists."""
        from redb.queries import _check_yara_matches_bulk

        mock_client = Mock()
        mock_client.query.return_value.result_rows = []
        mock_settings.create_clickhouse_client.return_value = mock_client

        # Create list larger than batch_size (3900)
        hashes = [f"{i:064x}" for i in range(4000)]
        _check_yara_matches_bulk(hashes)

        # Should have made 2 queries (3900 + 100)
        assert mock_client.query.call_count == 2


# ============================================================================
# Query Dispatch Tests (--analyzed + --date/--range flag combinations)
# ============================================================================

class TestIngestorQueryDispatch:
    """Tests that the ingestor dispatches to the correct query function
    based on flag combinations.

    Regression tests for a bug where `if self.analyzed` was checked before
    `if self.start_date and self.end_date`, causing --analyzed --date/--range
    to ignore the date filter and query all 4M+ samples from basic_properties.
    """

    @patch('redb.ingestor.multiprocessing')
    @patch('redb.ingestor.datetime')
    @patch('builtins.open', create=True)
    def _make_ingestor(self, mock_open, mock_datetime, mock_mp, **kwargs):
        """Helper to create an Ingestor with mocked multiprocessing/datetime."""
        mock_open.return_value.__enter__ = Mock()
        mock_open.return_value.__exit__ = Mock(return_value=False)
        mock_datetime.today.return_value.strftime.return_value = "20260222"

        from redb.ingestor import Ingestor
        defaults = dict(
            path=None, repository="analyzed", index_prefix="redb",
            s3_mode=True, force=True,
        )
        defaults.update(kwargs)
        return Ingestor(**defaults)

    @patch('redb.ingestor.fetch_analyzed_samples', return_value=[])
    @patch('redb.ingestor.fetch_s3_objects_by_date_range', return_value=[])
    def test_analyzed_with_date_uses_date_range_query(
        self, mock_date_range, mock_analyzed
    ):
        """--analyzed --date should use fetch_s3_objects_by_date_range, not fetch_analyzed_samples."""
        ingestor = self._make_ingestor(
            analyzed=True, start_date="2025-01-15", end_date="2025-01-16"
        )
        ingestor.ingest()

        mock_date_range.assert_called_once()
        mock_analyzed.assert_not_called()
        # Verify analyzed=True was passed through to the date range query
        call_kwargs = mock_date_range.call_args
        assert call_kwargs[1].get('analyzed') or call_kwargs[0][-1] if call_kwargs[0] else call_kwargs[1].get('analyzed') is True

    @patch('redb.ingestor.fetch_analyzed_samples', return_value=[])
    @patch('redb.ingestor.fetch_s3_objects_by_date_range', return_value=[])
    def test_analyzed_with_range_uses_date_range_query(
        self, mock_date_range, mock_analyzed
    ):
        """--analyzed --range should use fetch_s3_objects_by_date_range, not fetch_analyzed_samples."""
        ingestor = self._make_ingestor(
            analyzed=True, start_date="2025-01-01", end_date="2025-01-31"
        )
        ingestor.ingest()

        mock_date_range.assert_called_once()
        mock_analyzed.assert_not_called()

    @patch('redb.ingestor.fetch_analyzed_samples', return_value=[])
    @patch('redb.ingestor.fetch_s3_objects_by_date_range', return_value=[])
    def test_analyzed_standalone_uses_fetch_analyzed(
        self, mock_date_range, mock_analyzed
    ):
        """--analyzed without --date/--range should use fetch_analyzed_samples."""
        ingestor = self._make_ingestor(
            analyzed=True, start_date=None, end_date=None
        )
        ingestor.ingest()

        mock_analyzed.assert_called_once()
        mock_date_range.assert_not_called()

    @patch('redb.ingestor.fetch_analyzed_samples', return_value=[])
    @patch('redb.ingestor.fetch_s3_objects_by_date_range', return_value=[])
    def test_date_without_analyzed_uses_date_range_query(
        self, mock_date_range, mock_analyzed
    ):
        """--date without --analyzed should use fetch_s3_objects_by_date_range with analyzed=False."""
        ingestor = self._make_ingestor(
            analyzed=False, start_date="2025-01-15", end_date="2025-01-16"
        )
        ingestor.ingest()

        mock_date_range.assert_called_once()
        mock_analyzed.assert_not_called()

    @patch('redb.ingestor.fetch_analyzed_samples', return_value=[])
    @patch('redb.ingestor.fetch_s3_objects_by_date_range', return_value=[])
    def test_analyzed_with_date_passes_analyzed_flag(
        self, mock_date_range, mock_analyzed
    ):
        """--analyzed --date should pass analyzed=True to fetch_s3_objects_by_date_range."""
        ingestor = self._make_ingestor(
            analyzed=True, start_date="2025-01-15", end_date="2025-01-16"
        )
        ingestor.ingest()

        mock_date_range.assert_called_once()
        _, kwargs = mock_date_range.call_args
        assert kwargs.get('analyzed') is True

    @patch('redb.ingestor.fetch_analyzed_samples', return_value=[])
    @patch('redb.ingestor.fetch_s3_objects_by_date_range', return_value=[])
    def test_analyzed_with_date_passes_date_params(
        self, mock_date_range, mock_analyzed
    ):
        """--analyzed --date should pass correct start_date and end_date."""
        ingestor = self._make_ingestor(
            analyzed=True, start_date="2025-03-01", end_date="2025-03-15"
        )
        ingestor.ingest()

        mock_date_range.assert_called_once()
        _, kwargs = mock_date_range.call_args
        assert kwargs.get('start_date') == "2025-03-01"
        assert kwargs.get('end_date') == "2025-03-15"


# ============================================================================
# Helpers
# ============================================================================

def mock_open_binary(content):
    """Create a mock for open() that returns binary content."""
    mock = MagicMock()
    mock.return_value.__enter__ = Mock(return_value=Mock(read=Mock(return_value=content)))
    mock.return_value.__exit__ = Mock(return_value=False)
    return mock