from __future__ import annotations

import tempfile
import unittest
from pathlib import Path
from types import MethodType
from unittest.mock import ANY, patch

import torch

from psalm import psalm_model as psalm_model_module
from psalm import shell as psalm_shell
from psalm.cli import build_parser, run_scan_from_args, validate_scan_args
from psalm.fast_scan import _DecodeBundleResult, _DecodeResult, scan_fasta_fast
from psalm.psalm_model import PSALM, _DecodedDomains, _FinalizedScanResult, _PreparedDecodeInputs
from psalm.report import HitRow, write_hits_tsv


class _FakeModel:
    def __init__(self):
        self.batch_calls = []
        self.rendered_ids = []
        self.tsv_rows = None
        self.empty_hit_row_ids = set()
        self.finalize_dataset_sizes = []

    def _ensure_scan_ready(self) -> None:
        return None

    def batch_infer(self, sequences, max_batch_size=None):
        self.batch_calls.append(list(sequences))
        max_len = max(len(sequence) for sequence in sequences)
        return torch.zeros((len(sequences), max_len + 2, 4), dtype=torch.float32)

    def _prepare_decode_inputs(self, log_probs):
        return _PreparedDecodeInputs(
            total_fams=1,
            kept_families=1,
            keep_full_cpu=torch.tensor([True], dtype=torch.bool),
            sub_emits_cpu=torch.zeros((max(1, log_probs.shape[0]), 1), dtype=torch.float32),
            none_sub=0,
        )

    def _decode_prepared_inputs(
        self,
        *,
        prepared,
        sequence,
        beam_size,
        prior_mid_to_start,
        prior_stop_to_mid,
        prior_stop_to_start,
        refine_extended,
        include_verbose_artifacts,
    ):
        _ = (
            prepared,
            beam_size,
            prior_mid_to_start,
            prior_stop_to_mid,
            prior_stop_to_start,
            refine_extended,
            include_verbose_artifacts,
        )
        domain = ("PF00001", 1, len(sequence), float(len(sequence)), 1.0, 1.0, 0.0, "full")
        return _DecodedDomains(
            domains=[domain],
            domains_original=[],
            gamma=None,
            best_path=None,
            dec_dt=0.01,
        )

    def _finalize_scan_result(
        self,
        *,
        seq_id,
        decoded,
        dataset_size,
        score_thresh,
        evalue_thresh,
        include_verbose_artifacts,
    ):
        _ = (score_thresh, evalue_thresh, include_verbose_artifacts)
        self.finalize_dataset_sizes.append(dataset_size)
        domain = decoded.domains[0] if decoded.domains else ("PF00001", 1, 1, 0.0, 0.0, 1.0, 0.0, "full")
        hit_rows = []
        if seq_id not in self.empty_hit_row_ids and decoded.domains:
            hit_rows = [
                HitRow(
                    sequence=seq_id,
                    evalue=1.0e-5,
                    score=float(domain[3]),
                    pfam=domain[0],
                    start=domain[1],
                    stop=domain[2],
                    model="FakeFamily",
                    len_frac=float(domain[5]),
                    status="Full",
                    bit_score=float(domain[4]),
                    bias=float(domain[6]),
                    domain=domain,
                )
            ]
        return _FinalizedScanResult(
            domains_scored=decoded.domains,
            domains_original_scored=[],
            hit_rows=hit_rows,
            score_pass=len(decoded.domains),
            total_domains_raw=len(decoded.domains),
        )

    def _render_scan_output(
        self,
        *,
        sequence,
        seq_id,
        hit_rows,
        score_thresh,
        evalue_thresh,
        score_pass,
        total_domains_raw,
        total_fams,
        kept_families,
        domains_original_scored,
        gamma,
        best_path,
        verbose,
        refine_extended,
        beam_size,
        inf_dt,
        dec_dt,
        output=None,
    ) -> None:
        _ = (
            sequence,
            hit_rows,
            score_thresh,
            evalue_thresh,
            score_pass,
            total_domains_raw,
            total_fams,
            kept_families,
            domains_original_scored,
            gamma,
            best_path,
            verbose,
            refine_extended,
            beam_size,
            inf_dt,
            dec_dt,
            output,
        )
        self.rendered_ids.append(seq_id)
        stream = output if output is not None else None
        print(f"REPORT {seq_id}", file=stream)

    def _write_hits_tsv(self, path, rows) -> None:
        self.tsv_rows = list(rows)
        Path(path).write_text("\n".join(str(row) for row in rows), encoding="utf-8")

    def scan_fast(self, **kwargs):
        self.scan_fast_kwargs = kwargs
        return None


class _FakeWorkerPool:
    def __init__(self, size: int = 0, reverse_bundle_results: bool = False):
        self.target_size = size
        self.current_size = size
        self.ensure_size_calls = []
        self.submitted_bundles = []
        self.closed = False
        self._results = []
        self.reverse_bundle_results = reverse_bundle_results

    def ensure_size(self, target_size: int, status_callback=None) -> None:
        self.ensure_size_calls.append(target_size)
        if target_size > self.current_size and status_callback is not None:
            status_callback(f"Warming up {target_size - self.current_size} CPU decode worker(s)...")
        self.current_size = target_size
        self.target_size = target_size

    def submit_bundle(self, bundle) -> None:
        self.submitted_bundles.append(bundle)
        bundle_results = []
        tasks = list(bundle.tasks)
        if self.reverse_bundle_results:
            tasks = list(reversed(tasks))
        for task in tasks:
            bundle_results.append(
                _DecodeResult(
                    order_idx=task.order_idx,
                    seq_id=task.seq_id,
                    domains=[("PF00001", 1, len(task.sequence), float(len(task.sequence)), 1.0, 1.0, 0.0, "full")],
                    dec_dt=0.01,
                )
            )
        self._results.append(_DecodeBundleResult(results=bundle_results))

    def get_result(self, timeout: float):
        _ = timeout
        if self._results:
            return self._results.pop(0)
        return None

    def has_live_workers(self) -> bool:
        return self.current_size > 0

    def is_ready(self) -> bool:
        return self.current_size > 0

    def close(self) -> None:
        self.closed = True
        self.current_size = 0
        self.target_size = 0


class _FakeRegularScanModel:
    def __init__(self):
        self.scanned_ids = []
        self.scan = MethodType(PSALM.scan, self)

    def _scan_one(self, **kwargs):
        self.scanned_ids.append(kwargs["seq_id"])
        return []

    def _build_hit_rows(self, seq_id, domains, dataset_size):
        _ = (seq_id, domains, dataset_size)
        return []

    def _write_hits_tsv(self, path, rows):
        _ = (path, rows)


class FastScanTests(unittest.TestCase):
    def _write_fasta(self, contents: str) -> str:
        tmpdir = tempfile.TemporaryDirectory()
        self.addCleanup(tmpdir.cleanup)
        path = Path(tmpdir.name) / "input.fa"
        path.write_text(contents, encoding="utf-8")
        return str(path)

    def test_fast_scan_sorts_longest_first_and_batches_by_token_budget(self):
        fasta = self._write_fasta(">short\nAA\n>long\nAAAAA\n>mid\nAAA\n")
        model = _FakeModel()

        results = scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.2,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=True,
            cpu_workers=0,
            max_tokens_per_batch=10,
            _print_output=False,
        )

        self.assertIsNone(results)
        self.assertEqual(model.batch_calls, [["AAAAA"], ["AAA", "AA"]])

    def test_fast_scan_without_sort_preserves_fasta_order_in_output(self):
        fasta = self._write_fasta(">b\nAA\n>a\nAAAA\n")
        model = _FakeModel()
        tmpdir = tempfile.TemporaryDirectory()
        self.addCleanup(tmpdir.cleanup)
        txt_path = str(Path(tmpdir.name) / "report")

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=txt_path,
            sort=False,
            cpu_workers=0,
            max_tokens_per_batch=32,
            _print_output=False,
        )

        written = Path(f"{txt_path}.txt").read_text(encoding="utf-8")
        self.assertLess(written.index("REPORT b"), written.index("REPORT a"))

    def test_fast_scan_writes_non_verbose_reports_in_processing_order(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeModel()
        tmpdir = tempfile.TemporaryDirectory()
        self.addCleanup(tmpdir.cleanup)
        txt_path = str(Path(tmpdir.name) / "report")

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=txt_path,
            sort=True,
            cpu_workers=0,
            max_tokens_per_batch=32,
            _print_output=False,
        )

        written = Path(f"{txt_path}.txt").read_text(encoding="utf-8")
        self.assertIn("REPORT a", written)
        self.assertIn("REPORT b", written)
        self.assertLess(written.index("REPORT a"), written.index("REPORT b"))

    def test_write_hits_tsv_formats_score_and_evalue(self):
        tmpdir = tempfile.TemporaryDirectory()
        self.addCleanup(tmpdir.cleanup)
        path = Path(tmpdir.name) / "hits.tsv"
        row = HitRow(
            sequence="seq1",
            evalue=1.53e-34,
            score=0.9999995561881181,
            pfam="PF00118",
            start=1,
            stop=289,
            model="Cpn60_TCP1",
            len_frac=1.052158236503601,
            status="Partial (no stop)",
            bit_score=0.0,
            bias=0.0,
            domain=("PF00118", 1, 289, 0.9999995561881181, 0.0, 1.052158236503601, 0.0, "partial (no stop)"),
        )
        write_hits_tsv(str(path), [row])
        lines = path.read_text(encoding="utf-8").splitlines()
        self.assertEqual(lines[1].split("\t")[1], "1.5e-34")
        self.assertEqual(lines[1].split("\t")[2], "1.000")

    def test_fast_cli_keeps_T_as_score_threshold_for_fast_mode(self):
        parser = build_parser()
        args = parser.parse_args(["--fast", "-f", "proteins.fa", "-T", "0.2", "--sort", "-c", "4"])

        validate_scan_args(args)
        self.assertEqual(args.score_thresh, 0.2)
        self.assertEqual(args.cpu_workers, 4)

        model = _FakeModel()
        run_scan_from_args(model, args)
        self.assertEqual(model.scan_fast_kwargs["score_thresh"], 0.2)
        self.assertTrue(model.scan_fast_kwargs["sort"])
        self.assertEqual(model.scan_fast_kwargs["cpu_workers"], 4)
        self.assertFalse(model.scan_fast_kwargs["adaptive_fast"])

    def test_fast_cli_shows_progress_bar_even_when_quiet(self):
        parser = build_parser()
        args = parser.parse_args(["--fast", "-f", "proteins.fa", "-q"])

        validate_scan_args(args)
        model = _FakeModel()
        run_scan_from_args(model, args)
        self.assertFalse(model.scan_fast_kwargs["_print_output"])
        self.assertTrue(model.scan_fast_kwargs["_show_progress"])

    def test_fast_cli_default_max_tokens_per_batch_is_4096(self):
        parser = build_parser()
        args = parser.parse_args(["--fast", "-f", "proteins.fa"])

        validate_scan_args(args)
        self.assertEqual(args.max_tokens_per_batch, 4096)
        self.assertFalse(args.adaptive_fast)

    def test_fast_cli_passes_adaptive_flag(self):
        parser = build_parser()
        args = parser.parse_args(["--fast", "-f", "proteins.fa", "--adaptive-fast"])

        validate_scan_args(args)
        model = _FakeModel()
        run_scan_from_args(model, args)
        self.assertTrue(model.scan_fast_kwargs["adaptive_fast"])

    def test_shell_startup_parser_accepts_short_c_alias(self):
        parser = psalm_shell._build_main_parser()
        args = parser.parse_args(["-c", "3"])
        self.assertEqual(args.cpu_workers, 3)

    def test_fast_scan_uses_existing_worker_pool_target_when_unspecified(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeModel()
        worker_pool = _FakeWorkerPool(size=3)

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=True,
            cpu_workers=None,
            max_tokens_per_batch=32,
            max_queued_seqs=30,
            _print_output=False,
            _worker_pool_manager=worker_pool,
            _status_callback=lambda _msg: None,
        )

        self.assertEqual(worker_pool.ensure_size_calls, [])

    def test_fast_scan_rewarms_stale_existing_worker_pool(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeModel()
        worker_pool = _FakeWorkerPool(size=2)
        worker_pool.current_size = 0

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=True,
            cpu_workers=None,
            max_tokens_per_batch=32,
            max_queued_seqs=30,
            _print_output=False,
            _worker_pool_manager=worker_pool,
            _status_callback=lambda _msg: None,
        )

        self.assertEqual(worker_pool.ensure_size_calls, [2])

    def test_fast_scan_c0_disables_existing_worker_pool(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeModel()
        worker_pool = _FakeWorkerPool(size=2)

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=True,
            cpu_workers=0,
            max_tokens_per_batch=32,
            max_queued_seqs=30,
            _print_output=False,
            _worker_pool_manager=worker_pool,
            _status_callback=lambda _msg: None,
        )

        self.assertEqual(worker_pool.ensure_size_calls, [0])
        self.assertEqual(worker_pool.submitted_bundles, [])

    def test_fast_scan_defaults_to_main_process_without_pool(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeModel()

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=True,
            cpu_workers=None,
            max_tokens_per_batch=32,
            max_queued_seqs=None,
            _print_output=False,
            _worker_pool_manager=None,
            _status_callback=lambda _msg: None,
        )

        self.assertEqual(model.batch_calls, [["AAAA", "AA"]])

    def test_fast_scan_bundles_multiple_sequences_per_queue_item(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAAA\n>c\nAA\n>d\nA\n")
        model = _FakeModel()
        worker_pool = _FakeWorkerPool(size=2)

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=True,
            cpu_workers=2,
            max_tokens_per_batch=128,
            max_queued_seqs=40,
            _print_output=False,
            _worker_pool_manager=worker_pool,
            _status_callback=lambda _msg: None,
        )

        self.assertTrue(worker_pool.submitted_bundles)
        self.assertTrue(any(len(bundle.tasks) > 1 for bundle in worker_pool.submitted_bundles))
        shared_tasks = [
            task
            for bundle in worker_pool.submitted_bundles
            for task in bundle.tasks
        ]
        self.assertTrue(shared_tasks)
        self.assertTrue(all(task.prepared.keep_full_cpu.is_shared() for task in shared_tasks))
        self.assertTrue(
            all(
                task.prepared.sub_emits_cpu is not None
                and task.prepared.sub_emits_cpu.is_shared()
                for task in shared_tasks
            )
        )

    def test_fast_scan_emits_in_order_even_when_worker_results_arrive_reversed(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAAA\n")
        model = _FakeModel()
        tmpdir = tempfile.TemporaryDirectory()
        self.addCleanup(tmpdir.cleanup)
        txt_path = str(Path(tmpdir.name) / "report")
        worker_pool = _FakeWorkerPool(size=2, reverse_bundle_results=True)

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=txt_path,
            sort=True,
            cpu_workers=2,
            max_tokens_per_batch=128,
            max_queued_seqs=40,
            _print_output=False,
            _worker_pool_manager=worker_pool,
            _status_callback=lambda _msg: None,
        )

        written = Path(f"{txt_path}.txt").read_text(encoding="utf-8")
        self.assertLess(written.index("REPORT a"), written.index("REPORT b"))

    def test_fast_scan_does_not_rewarm_when_requested_size_matches_pool(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAAA\n")
        model = _FakeModel()
        worker_pool = _FakeWorkerPool(size=4)

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=True,
            cpu_workers=4,
            max_tokens_per_batch=128,
            max_queued_seqs=40,
            _print_output=False,
            _worker_pool_manager=worker_pool,
            _status_callback=lambda _msg: None,
        )

        self.assertEqual(worker_pool.ensure_size_calls, [])

    def test_shell_startup_warmup_runs_through_startup_callback(self):
        fake_model = _FakeModel()
        with patch.object(psalm_shell, "load_model_with_startup") as mocked_loader:
            def _fake_load_model_with_startup(**kwargs):
                callback = kwargs["extra_setup_callback"]
                callback(fake_model, lambda _msg: None)
                return fake_model

            mocked_loader.side_effect = _fake_load_model_with_startup
            with patch.object(psalm_shell, "FastWorkerPoolManager") as mocked_manager:
                manager_instance = mocked_manager.return_value
                with patch("sys.argv", ["psalm", "-c", "2"]):
                    with patch("builtins.input", side_effect=["quit"]):
                        psalm_shell.main()
                manager_instance.ensure_size.assert_called_once_with(2, status_callback=ANY)

    def test_fast_scan_streams_tsv_and_skips_sequences_without_hits(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeModel()
        model.empty_hit_row_ids.add("b")
        tmpdir = tempfile.TemporaryDirectory()
        self.addCleanup(tmpdir.cleanup)
        path = str(Path(tmpdir.name) / "fast.tsv")

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=path,
            to_txt=None,
            sort=False,
            cpu_workers=0,
            max_tokens_per_batch=32,
            _print_output=False,
        )

        lines = Path(path).read_text(encoding="utf-8").splitlines()
        self.assertEqual(len(lines), 2)
        self.assertIn("a", lines[1])
        self.assertNotIn("b", "\n".join(lines[1:]))

    def test_fast_scan_uses_counted_dataset_size_when_unspecified(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeModel()

        scan_fasta_fast(
            model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=False,
            cpu_workers=0,
            max_tokens_per_batch=32,
            _print_output=False,
        )

        self.assertEqual(model.finalize_dataset_sizes, [2.0, 2.0])

    def test_fast_scan_explicit_dataset_size_matches_unspecified_count(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        counted_model = _FakeModel()
        explicit_model = _FakeModel()

        scan_fasta_fast(
            counted_model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=None,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=False,
            cpu_workers=0,
            max_tokens_per_batch=32,
            _print_output=False,
        )
        scan_fasta_fast(
            explicit_model,
            fasta=fasta,
            score_thresh=0.0,
            beam_size=64,
            dataset_size=2,
            evalue_thresh=0.01,
            prior_mid_to_start=3.4e-5,
            prior_stop_to_mid=4.817e-3,
            prior_stop_to_start=5.540e-3,
            refine_extended=True,
            to_tsv=None,
            to_txt=None,
            sort=False,
            cpu_workers=0,
            max_tokens_per_batch=32,
            _print_output=False,
        )

        self.assertEqual(counted_model.finalize_dataset_sizes, explicit_model.finalize_dataset_sizes)

    def test_non_fast_multi_fasta_uses_progress_bar_with_counted_total(self):
        fasta = self._write_fasta(">a\nAAAA\n>b\nAA\n")
        model = _FakeRegularScanModel()
        with patch.object(psalm_model_module, "tqdm") as mock_tqdm:
            progress = mock_tqdm.return_value
            with patch.object(psalm_model_module.sys.stderr, "isatty", return_value=True):
                result = model.scan(fasta=fasta, _print_output=False)

        self.assertEqual(result, {"a": [], "b": []})
        self.assertEqual(model.scanned_ids, ["a", "b"])
        mock_tqdm.assert_called_once()
        self.assertEqual(mock_tqdm.call_args.kwargs["total"], 2)
        self.assertEqual(progress.update.call_count, 2)
        progress.close.assert_called_once()

    def test_fast_cli_rejects_sequence_input(self):
        parser = build_parser()
        args = parser.parse_args(["--fast", "-s", "AAAA"])

        with self.assertRaises(ValueError):
            validate_scan_args(args)


if __name__ == "__main__":
    unittest.main()
