from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
import tempfile
import threading
import unittest
from pathlib import Path

import doc2md.cli as cli
from doc2md import history_db


ROOT = Path(__file__).resolve().parents[1]
FIX_IN = ROOT / 'tests' / 'fixtures' / 'in'
FIX_OUT = ROOT / 'tests' / 'fixtures' / 'out'


def run_cli(args: list[str]) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        [sys.executable, '-m', 'doc2md.cli', *args],
        cwd=ROOT,
        text=True,
        capture_output=True,
    )


class CliBehaviorTest(unittest.TestCase):
    def test_stdout_default(self) -> None:
        expected = (FIX_OUT / 'sample.csv.md').read_text(encoding='utf-8')
        output_path = ROOT / 'docs_out' / 'sample.csv.md'
        if output_path.exists():
            output_path.unlink()
        result = run_cli([str(FIX_IN / 'sample.csv')])
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stdout, '')
        self.assertIn('Converted sample.csv', result.stderr)
        self.assertTrue(output_path.exists())
        self.assertEqual(output_path.read_text(encoding='utf-8'), expected)
        output_path.unlink()

    def test_output_file(self) -> None:
        expected = (FIX_OUT / 'sample.csv.md').read_text(encoding='utf-8')
        with tempfile.TemporaryDirectory() as tmpdir:
            out_path = Path(tmpdir) / 'out.md'
            result = run_cli([str(FIX_IN / 'sample.csv'), '-o', str(out_path)])
            self.assertEqual(result.returncode, 0)
            self.assertEqual(result.stdout, '')
            self.assertIn('Converted sample.csv', result.stderr)
            self.assertEqual(out_path.read_text(encoding='utf-8'), expected)

    def test_output_dash_stdout(self) -> None:
        expected = (FIX_OUT / 'sample.csv.md').read_text(encoding='utf-8')
        result = run_cli([str(FIX_IN / 'sample.csv'), '-o', '-'])
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stdout, expected)
        self.assertIn('Converted sample.csv', result.stderr)

    def test_batch_requires_yes(self) -> None:
        inputs = [FIX_IN / 'sample.csv', FIX_IN / 'edge.csv']
        with tempfile.TemporaryDirectory() as tmpdir:
            out_dir = Path(tmpdir) / 'out'
            args = argparse.Namespace(
                format=None,
                engine='pdfminer',
                output=str(out_dir),
                preview=False,
                diff=False,
                yes=False,
                stats=False,
                profile_report=None,
                theme="auto",
                quiet=True,
                debug=False,
                csv_na='',
                csv_float_format=None,
                ocr=False,
                ocr_mode="never",
                ocr_lang="es",
                ocr_device=None,
                ocr_render_scale=2.0,
                ocr_min_score=None,
            )

            exit_code = cli._run_conversion(inputs, args, lambda _msg: None)
            self.assertEqual(exit_code, 2)
            self.assertFalse(out_dir.exists())

            args.yes = True
            exit_code_yes = cli._run_conversion(inputs, args, lambda _msg: None)
            self.assertEqual(exit_code_yes, 0)
            self.assertTrue((out_dir / 'sample.csv.md').exists())
            self.assertTrue((out_dir / 'edge.csv.md').exists())

    def test_preview_multiple_inputs_exit_2(self) -> None:
        args = argparse.Namespace(
            format=None,
            engine='pdfminer',
            output=None,
            preview=True,
            diff=False,
            yes=False,
            stats=False,
            profile_report=None,
            theme="auto",
            quiet=True,
            debug=False,
            csv_na='',
            csv_float_format=None,
            ocr=False,
            ocr_mode="never",
            ocr_lang="es",
            ocr_device=None,
            ocr_render_scale=2.0,
            ocr_min_score=None,
        )
        exit_code = cli._run_conversion(
            [FIX_IN / 'sample.csv', FIX_IN / 'edge.csv'],
            args,
            lambda _msg: None,
        )
        self.assertEqual(exit_code, 2)

    def test_preview_unsupported_format_exit_3(self) -> None:
        result = run_cli([str(FIX_IN / 'sample.txt'), '--preview'])
        self.assertEqual(result.returncode, 3)
        self.assertIn('Unsupported', result.stderr)

    def test_preview_missing_file_exit_4(self) -> None:
        result = run_cli([str(FIX_IN / 'missing.pdf'), '--preview'])
        self.assertEqual(result.returncode, 4)

    def test_diff_requires_yes_exit_5(self) -> None:
        with tempfile.TemporaryDirectory() as tmpdir:
            out_path = Path(tmpdir) / 'out.md'
            out_path.write_text('old', encoding='utf-8')
            result = run_cli(
                [str(FIX_IN / 'sample.csv'), '-o', str(out_path), '--diff']
            )
            self.assertEqual(result.returncode, 5)
            self.assertIn('---', result.stderr)
            self.assertIn('Refusing to overwrite', result.stderr)

    def test_preview_outputs_to_stdout(self) -> None:
        expected = (FIX_OUT / 'sample.csv.md').read_text(encoding='utf-8')
        output_path = ROOT / 'docs_out' / 'sample.csv.md'
        if output_path.exists():
            output_path.unlink()
        result = run_cli([str(FIX_IN / 'sample.csv'), '--preview', '--quiet'])
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stdout, expected)
        self.assertEqual(result.stderr, '')
        self.assertFalse(output_path.exists())

    def test_overwrite_requires_yes(self) -> None:
        expected = (FIX_OUT / 'sample.csv.md').read_text(encoding='utf-8')
        with tempfile.TemporaryDirectory() as tmpdir:
            out_path = Path(tmpdir) / 'out.md'
            out_path.write_text('old', encoding='utf-8')
            result = run_cli([str(FIX_IN / 'sample.csv'), '-o', str(out_path)])
            self.assertEqual(result.returncode, 5)
            self.assertIn('Refusing to overwrite', result.stderr)
            self.assertEqual(out_path.read_text(encoding='utf-8'), 'old')

            result_yes = run_cli(
                [str(FIX_IN / 'sample.csv'), '-o', str(out_path), '--yes', '--quiet']
            )
            self.assertEqual(result_yes.returncode, 0)
            self.assertEqual(out_path.read_text(encoding='utf-8'), expected)

    def test_diff_emits_unified_diff(self) -> None:
        with tempfile.TemporaryDirectory() as tmpdir:
            out_path = Path(tmpdir) / 'out.md'
            out_path.write_text('old', encoding='utf-8')
            result = run_cli(
                [str(FIX_IN / 'sample.csv'), '-o', str(out_path), '--diff', '--yes']
            )
            self.assertEqual(result.returncode, 0)
            self.assertIn('---', result.stderr)
            self.assertIn('+++', result.stderr)

    def test_slash_commands_require_interactive(self) -> None:
        result = run_cli(['/format:csv'])
        self.assertEqual(result.returncode, 2)
        self.assertIn('Slash commands are only available in interactive mode', result.stderr)

    def test_debug_mode(self) -> None:
        output_path = ROOT / 'docs_out' / 'sample.csv.md'
        if output_path.exists():
            output_path.unlink()
        result = run_cli([str(FIX_IN / 'sample.csv'), '--debug'])
        self.assertEqual(result.returncode, 0)
        self.assertIn('DEBUG', result.stderr)
        self.assertIn('Converted sample.csv', result.stderr)
        self.assertTrue(output_path.exists())
        output_path.unlink()

    def test_version_flag(self) -> None:
        result = run_cli(['--version'])
        self.assertEqual(result.returncode, 0)
        self.assertIn('doc2md', result.stdout.lower())
        self.assertEqual(result.stderr, '')

    def test_slash_command_suggestions(self) -> None:
        self.assertEqual(cli._slash_command_candidates('/'), cli.CORE_SLASH_COMMANDS)
        self.assertEqual(cli._slash_command_candidates('/f'), ['/files', '/format'])
        self.assertEqual(cli._slash_command_candidates('/m'), ['/more'])
        self.assertEqual(cli._slash_command_candidates('/x'), [])
        self.assertEqual(cli._slash_command_candidates('f'), [])
        self.assertEqual(cli._slash_command_candidates('/format foo'), [])
        items = cli._slash_command_items('/f')
        self.assertEqual(
            items,
            [
                ('/files', cli.SLASH_COMMAND_DETAILS['/files']),
                ('/format', cli.SLASH_COMMAND_DETAILS['/format']),
            ],
        )

    def test_slash_option_suggestions(self) -> None:
        options = cli._slash_option_items('/format')
        self.assertIn(('pdf', 'Force PDF format'), options)
        self.assertIn(('image', 'Force image format'), options)
        self.assertIn(('auto', 'Clear format override'), options)
        self.assertEqual(cli._slash_option_items('/format p'), [('pdf', 'Force PDF format')])
        self.assertEqual(cli._slash_option_items('/engine m'), [('marker', 'Marker PDF engine')])
        self.assertEqual(cli._slash_option_items('/output -'), [('-', 'Write to stdout')])
        self.assertEqual(cli._slash_option_items('/ocr mode au'), [('auto', 'OCR only when PDF text is empty')])
        more = cli._slash_option_items('/more')
        self.assertIn(('history', 'History tools'), more)
        self.assertIn(('session', 'Session toggles'), more)

    def test_detect_format_image(self) -> None:
        self.assertEqual(cli._detect_format('sample.png'), 'image')

    def test_slash_command_ignores_image_path(self) -> None:
        self.assertFalse(cli._looks_like_slash_command('/docs_in/sample.png'))

    def test_no_input_flag(self) -> None:
        result = run_cli(['--no-input'])
        self.assertEqual(result.returncode, 2)
        self.assertIn('Input is required', result.stderr)

    def test_quiet_suppresses_debug(self) -> None:
        result = run_cli([str(FIX_IN / 'sample.csv'), '--debug', '--quiet'])
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stderr, '')

    def test_history_path(self) -> None:
        self.assertEqual(cli._history_path().name, '.doc2md_history')

    def test_prompt_settings(self) -> None:
        self.assertEqual(cli._clamp_index(-1, 3), 0)
        self.assertEqual(cli._clamp_index(5, 3), 2)
        self.assertEqual(cli._clamp_index(0, 0), 0)

    def test_command_buffer_text(self) -> None:
        self.assertEqual(cli._command_buffer_text('/format'), '/format ')
        self.assertEqual(cli._command_buffer_text('/ocr'), '/ocr ')
        self.assertEqual(cli._command_buffer_text('/help'), '/help')

    def test_command_list_rendering(self) -> None:
        items = [('/help', 'Show available commands')]
        lines = cli._render_command_list(items, 0)
        combined = ''.join(text for _, text in lines)
        self.assertIn('help', combined)
        self.assertIn('Show available commands', combined)
        self.assertTrue(cli._command_count_text(items, 0).startswith('▾'))

    def test_file_list_rendering(self) -> None:
        files = [Path('a.pdf'), Path('b.docx')]
        lines = cli._render_file_list(files, 1, {files[1]})
        combined = ''.join(text for _, text in lines)
        self.assertIn('[ ] a.pdf', combined)
        self.assertIn('[x] b.docx', combined)
        self.assertTrue(cli._file_count_text(files, {files[1]}).startswith('▾'))

    def test_load_input_files_filters_supported(self) -> None:
        with tempfile.TemporaryDirectory() as tmpdir:
            base = Path(tmpdir)
            (base / 'a.pdf').write_text('a', encoding='utf-8')
            (base / 'b.docx').write_text('b', encoding='utf-8')
            (base / 'c.csv').write_text('c', encoding='utf-8')
            (base / 'd.txt').write_text('d', encoding='utf-8')
            (base / 'e.png').write_text('e', encoding='utf-8')
            files = cli._load_input_files(base)
            self.assertEqual(
                [path.name for path in files],
                ['a.pdf', 'b.docx', 'c.csv', 'e.png'],
            )

    def test_progress_bar_text(self) -> None:
        bar = cli._progress_bar_text(1, 4, width=10)
        self.assertIn('%', bar)
        self.assertIn('[', bar)
        self.assertIn(']', bar)

    def test_help_text_rendering(self) -> None:
        lines = cli._render_help_text()
        combined = ''.join(text for _, text in lines)
        self.assertIn('documint2md interactive help', combined)
        self.assertIn('/help', combined)

    def test_settings_summary(self) -> None:
        args = argparse.Namespace(
            format=None,
            engine='pdfminer',
            output=None,
            ocr_mode="never",
            ocr_lang="es",
        )
        summary = cli._settings_summary(args)
        self.assertIn('Fmt:auto', summary)
        self.assertIn('Eng:pdfminer', summary)
        self.assertIn('Out:docs_out', summary)

    def test_default_options_for_format(self) -> None:
        self.assertEqual(
            cli._default_options_for_format('pdf'),
            {'engine': 'pdfminer'},
        )
        self.assertEqual(
            cli._default_options_for_format('csv'),
            {'csv_na': '', 'csv_float_format': None},
        )
        self.assertEqual(cli._default_options_for_format('docx'), {})

    def test_profile_applies_defaults_to_run(self) -> None:
        sample = FIX_IN / 'sample.csv'
        with tempfile.TemporaryDirectory() as tmpdir:
            temp_path = Path(tmpdir)
            (temp_path / 'doc2md.toml').write_text(
                "\n".join(
                    [
                        "[profiles.default]",
                        "format = \"csv\"",
                        "",
                        "[profiles.fast]",
                        "output = \"out.md\"",
                        "quiet = true",
                        "",
                    ]
                ),
                encoding='utf-8',
            )
            env = os.environ.copy()
            existing = env.get("PYTHONPATH")
            if existing:
                env["PYTHONPATH"] = f"{ROOT}{os.pathsep}{existing}"
            else:
                env["PYTHONPATH"] = str(ROOT)
            result = subprocess.run(
                [sys.executable, "-m", "doc2md.cli", str(sample), "--profile", "fast"],
                cwd=temp_path,
                text=True,
                capture_output=True,
                env=env,
            )
            self.assertEqual(result.returncode, 0)
            self.assertEqual(result.stderr, "")
            self.assertTrue((temp_path / "out.md").exists())

    def test_theme_resolution(self) -> None:
        self.assertEqual(cli._resolve_theme_name("light", None), "light")
        self.assertEqual(cli._resolve_theme_name("auto", None), "dark")
        self.assertEqual(cli._resolve_theme_name("auto", 1), "minimal")
        self.assertEqual(cli._resolve_theme_name("auto", 4), "minimal")
        self.assertEqual(cli._resolve_theme_name("auto", 8), "dark")

    def test_render_banner_text(self) -> None:
        fragments = cli._render_banner_text(
            "documint2md",
            "1.2.3",
            Path("C:/work/documint2md"),
            width=60,
        )
        combined = "".join(text for _, text in fragments)
        self.assertIn("documint2md", combined)
        self.assertIn("v1.2.3", combined)
        self.assertIn("Project", combined)
        self.assertIn("Enter", combined)

    def test_render_hero_text(self) -> None:
        fragments = cli._render_hero_text(
            "documint2md",
            "1.2.3",
            Path("C:/work/documint2md"),
            "docs_out",
            ["No recent activity yet"],
            width=80,
        )
        combined = "".join(text for _, text in fragments)
        self.assertIn("documint2md", combined)
        self.assertIn("Welcome back", combined)
        self.assertIn("Tips for getting started", combined)
        self.assertIn("Recent activity", combined)

    def test_recent_activity_lines_with_run(self) -> None:
        with tempfile.TemporaryDirectory() as tmpdir:
            path = Path(tmpdir) / "history.sqlite"
            logger = history_db.HistoryLogger(path)
            run_id = logger.start_run(
                engine="pdfminer",
                fmt="pdf",
                options={},
                cwd="C:/work/documint2md",
                version="1.0.1",
            )
            logger.add_input(
                run_id,
            path=Path("C:/work/documint2md/sample.pdf"),
                sha256="0" * 64,
                size_bytes=12,
                mtime_ns=0,
            )
            logger.finish_run(run_id, status="ok", exit_code=0)
            logger.close()
            lines = cli._recent_activity_lines(history_path=path)
            self.assertTrue(lines)
            self.assertIn("Last run #", lines[0])
            self.assertIn("file", lines[0])
            self.assertIn("pdf", lines[1])

    def test_stats_outputs_to_stderr(self) -> None:
        output_path = ROOT / 'docs_out' / 'sample.csv.md'
        if output_path.exists():
            output_path.unlink()
        result = run_cli(
            [str(FIX_IN / 'sample.csv'), '--stats', '--quiet', '--yes']
        )
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stdout, '')
        self.assertIn('Stats:', result.stderr)
        self.assertIn('convert_ms', result.stderr)

    def test_profile_report_writes_json(self) -> None:
        with tempfile.TemporaryDirectory() as tmpdir:
            out_path = Path(tmpdir) / 'out.md'
            report_path = Path(tmpdir) / 'report.json'
            result = run_cli(
                [
                    str(FIX_IN / 'sample.csv'),
                    '-o',
                    str(out_path),
                    '--profile-report',
                    str(report_path),
                    '--quiet',
                ]
            )
            self.assertEqual(result.returncode, 0)
            self.assertTrue(report_path.exists())
            report = json.loads(report_path.read_text(encoding='utf-8'))
            self.assertIn('inputs', report)
            self.assertEqual(len(report['inputs']), 1)
            self.assertIn('stages_ms', report['inputs'][0])

    def test_output_label(self) -> None:
        self.assertEqual(cli._output_label(None), 'docs_out')
        self.assertEqual(cli._output_label('-'), 'stdout')
        self.assertEqual(cli._output_label('out.md'), 'out.md')

    def test_split_slash_text(self) -> None:
        self.assertEqual(cli._split_slash_text('/format pdf'), ('format', 'pdf'))
        self.assertEqual(cli._split_slash_text('/format'), ('format', ''))

    def test_files_command_has_no_options(self) -> None:
        self.assertEqual(cli._slash_option_items('/files'), [])


class InteractiveUiTest(unittest.TestCase):
    def _make_args(self) -> argparse.Namespace:
        return argparse.Namespace(
            format=None,
            engine='pdfminer',
            output=None,
            preview=False,
            diff=False,
            yes=False,
            stats=False,
            profile_report=None,
            theme="auto",
            quiet=False,
            debug=False,
            csv_na='',
            csv_float_format=None,
            ocr=False,
            ocr_mode="never",
            ocr_lang="es",
            ocr_device=None,
            ocr_render_scale=2.0,
            ocr_min_score=None,
        )

    def test_interactive_help_sets_status(self) -> None:
        try:
            from prompt_toolkit.input.defaults import create_pipe_input
            from prompt_toolkit.output import DummyOutput
        except Exception:
            self.skipTest('prompt_toolkit not installed')
        args = self._make_args()

        def debug(_msg: str) -> None:
            return

        with create_pipe_input() as pipe_input:
            app, state = cli._build_interactive_app(
                args,
                debug,
                input=pipe_input,
                output=DummyOutput(),
            )

            def feed() -> None:
                pipe_input.send_text('/help')
                pipe_input.send_text('\r')
                pipe_input.send_text('\x1b')

            thread = threading.Thread(target=feed)
            thread.start()
            app.run()
            thread.join()

        self.assertIn('Help:', state['status'])
        self.assertEqual(state['mode'], 'help')

    def test_interactive_files_command(self) -> None:
        try:
            from prompt_toolkit.input.defaults import create_pipe_input
            from prompt_toolkit.output import DummyOutput
        except Exception:
            self.skipTest('prompt_toolkit not installed')
        args = self._make_args()

        def debug(_msg: str) -> None:
            return

        with tempfile.TemporaryDirectory() as tmpdir:
            base = Path(tmpdir)
            (base / 'sample.pdf').write_text('data', encoding='utf-8')
            original = cli.DEFAULT_INPUT_DIR
            cli.DEFAULT_INPUT_DIR = base
            try:
                with create_pipe_input() as pipe_input:
                    app, state = cli._build_interactive_app(
                        args,
                        debug,
                        input=pipe_input,
                        output=DummyOutput(),
                    )

                    def feed() -> None:
                        pipe_input.send_text('/files')
                        pipe_input.send_text('\r')
                        pipe_input.send_text('\x1b')

                    thread = threading.Thread(target=feed)
                    thread.start()
                    app.run()
                    thread.join()
            finally:
                cli.DEFAULT_INPUT_DIR = original

        self.assertEqual(state['mode'], 'files')
        self.assertIn('Select files', state['status'])

    def test_unsupported_format(self) -> None:
        result = run_cli([str(FIX_IN / 'sample.txt')])
        self.assertEqual(result.returncode, 3)
        self.assertIn('Unsupported', result.stderr)

    def test_missing_file(self) -> None:
        result = run_cli([str(FIX_IN / 'missing.pdf')])
        self.assertEqual(result.returncode, 4)

    def test_engine_flag_for_non_pdf(self) -> None:
        result = run_cli([str(FIX_IN / 'sample.csv'), '--engine', 'pdftext'])
        self.assertEqual(result.returncode, 2)
        self.assertIn('--engine is only valid for PDF', result.stderr)


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