from pathlib import Path
import json
import os
import subprocess
import sys
import zipfile

from typer.testing import CliRunner

from sentra.cli import app

runner = CliRunner()


def test_root_invocation_shows_guided_overview() -> None:
    result = runner.invoke(app, [])

    assert result.exit_code == 0
    assert "Sentra Overview" in result.stdout
    assert "quick-scan" in result.stdout
    assert "What Sentra does" in result.stdout
    assert "Run first" in result.stdout


def test_root_version_flag_reports_version() -> None:
    result = runner.invoke(app, ["--version"])

    assert result.exit_code == 0
    assert "sentra 0.1.0" in result.stdout


def test_full_cli_workflow(tmp_path: Path) -> None:
    firmware_path = tmp_path / "sample_firmware.bin"
    firmware_path.write_text("acme password telnet", encoding="utf-8")

    net_result = runner.invoke(
        app,
        ["scan", "net", "--target", "192.168.10.0/24", "--workspace", str(tmp_path), "--yes"],
    )
    assert net_result.exit_code == 0
    assert "Network Scan Results" in net_result.stdout
    assert "Saved network scan" in net_result.stdout
    assert "Network Scan Summary" in net_result.stdout

    firmware_result = runner.invoke(
        app,
        ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)],
    )
    assert firmware_result.exit_code == 0
    assert "Firmware Scan Results" in firmware_result.stdout
    assert "Saved firmware scan" in firmware_result.stdout
    assert "Firmware Scan Summary" in firmware_result.stdout

    correlate_result = runner.invoke(app, ["correlate", "--workspace", str(tmp_path)])
    assert correlate_result.exit_code == 0
    assert "Evidence graph correlation complete." in correlate_result.stdout
    assert "Evidence Graph" in correlate_result.stdout
    assert "Correlation Summary" in correlate_result.stdout

    report_result = runner.invoke(
        app,
        ["report", "--workspace", str(tmp_path), "--format", "markdown"],
    )
    assert report_result.exit_code == 0
    assert "Saved markdown report" in report_result.stdout
    assert "Report Summary" in report_result.stdout
    assert (tmp_path / ".sentra" / "report.md").exists()


def test_terminal_report_without_prior_artifacts(tmp_path: Path) -> None:
    result = runner.invoke(app, ["report", "--workspace", str(tmp_path)])
    assert result.exit_code == 0
    assert "No evidence artifacts available yet." in result.stdout
    assert "Report Summary" in result.stdout


def test_long_scan_prompt_can_cancel_before_running(tmp_path: Path) -> None:
    result = runner.invoke(
        app,
        ["scan", "net", "--target", "192.168.10.0/24", "--workspace", str(tmp_path)],
        input="n\n",
    )

    assert result.exit_code == 1
    assert not (tmp_path / ".sentra" / "network_scan.json").exists()


def test_invalid_report_format_uses_friendly_error_panel(tmp_path: Path) -> None:
    result = runner.invoke(app, ["report", "--workspace", str(tmp_path), "--format", "xml"])

    assert result.exit_code == 1


def test_scan_firmware_json_mode_is_script_friendly(tmp_path: Path) -> None:
    firmware_path = tmp_path / "firmware.bin"
    firmware_path.write_text("acme password", encoding="utf-8")

    result = runner.invoke(
        app,
        ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path), "--json"],
    )

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    assert payload["path"] == str(firmware_path)
    assert "Firmware Scan Results" not in result.stdout
    assert "Saved firmware scan" not in result.stdout


def test_scan_passive_parses_summary_file(tmp_path: Path) -> None:
    summary_file = tmp_path / "passive-summary.txt"
    summary_file.write_text(
        "\n".join(
            [
                "mDNS announce from 192.168.1.5 _http._tcp.local",
                "SSDP notify from 192.168.1.6 upnp rootDesc.xml",
            ]
        ),
        encoding="utf-8",
    )

    result = runner.invoke(
        app,
        ["scan", "passive", "--summary-file", str(summary_file), "--workspace", str(tmp_path)],
    )

    assert result.exit_code == 0
    assert "Parsed 2 passive observation" in result.stdout
    assert (tmp_path / ".sentra" / "passive_scan.json").exists()


def test_correlate_markdown_mode_emits_markdown(tmp_path: Path) -> None:
    firmware_path = tmp_path / "firmware.bin"
    firmware_path.write_text("northwind telnet", encoding="utf-8")

    runner.invoke(
        app,
        ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)],
    )

    result = runner.invoke(app, ["correlate", "--workspace", str(tmp_path), "--markdown"])

    assert result.exit_code == 0
    assert result.stdout.startswith("# Evidence Graph Summary")
    assert "Saved correlation graph" not in result.stdout


def test_scan_firmware_quiet_suppresses_summary_noise(tmp_path: Path) -> None:
    firmware_path = tmp_path / "firmware.bin"
    firmware_path.write_text("acme password", encoding="utf-8")

    result = runner.invoke(
        app,
        ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path), "--quiet"],
    )

    assert result.exit_code == 0
    assert "Firmware Scan Results" in result.stdout
    assert "Saved firmware scan" not in result.stdout
    assert "Firmware Scan Summary" not in result.stdout


def test_scan_firmware_verbose_shows_extra_details(tmp_path: Path) -> None:
    firmware_path = tmp_path / "firmware.bin"
    firmware_path.write_text("acme password", encoding="utf-8")

    result = runner.invoke(
        app,
        ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path), "--verbose"],
    )

    assert result.exit_code == 0
    assert "Workspace:" in result.stdout
    assert str(tmp_path) in result.stdout
    assert "Artifact path:" in result.stdout


def test_wizard_scan_only_exports_artifact(tmp_path: Path) -> None:
    export_path = tmp_path / "wizard-network.json"

    result = runner.invoke(
        app,
        ["wizard"],
        input=f"network\n127.0.0.1\n{export_path}\nn\n",
    )

    assert result.exit_code == 0
    assert "Sentra Wizard" in result.stdout
    assert "Target type" in result.stdout
    assert "Output path (file path, or directory ending with a path separator)" in result.stdout
    assert "Exported network scan" in result.stdout
    assert export_path.exists()
    assert (tmp_path / ".sentra" / "network_scan.json").exists()


def test_wizard_trims_inputs_and_accepts_directory_output(tmp_path: Path) -> None:
    output_dir = tmp_path / "exports"
    output_dir.mkdir()
    separator = "\\" if sys.platform.startswith("win") else "/"

    result = runner.invoke(
        app,
        ["wizard"],
        input=f" network \n 127.0.0.1 \n {output_dir}{separator}\n n\n",
    )

    assert result.exit_code == 0
    assert (output_dir / "network_scan.json").exists()


def test_wizard_directory_input_creates_default_report_filename(tmp_path: Path) -> None:
    firmware_path = tmp_path / "wizard_firmware.bin"
    firmware_path.write_text("northwind password telnet", encoding="utf-8")
    output_dir = tmp_path / "wizard-exports"
    output_dir.mkdir()

    result = runner.invoke(
        app,
        ["wizard"],
        input=f"firmware\n{firmware_path}\n{output_dir}\ny\n",
    )

    assert result.exit_code == 0
    assert "Saved markdown report" in result.stdout
    assert (output_dir / "report.md").exists()
    assert (output_dir / ".sentra" / "firmware_scan.json").exists()
    assert (output_dir / ".sentra" / "correlation.json").exists()


def test_wizard_file_input_uses_path_directly(tmp_path: Path) -> None:
    firmware_path = tmp_path / "wizard_firmware.bin"
    firmware_path.write_text("northwind password telnet", encoding="utf-8")
    report_path = tmp_path / "wizard-report"

    result = runner.invoke(
        app,
        ["wizard"],
        input=f"firmware\n{firmware_path}\n{report_path}\ny\n",
    )

    assert result.exit_code == 0
    assert "Firmware Scan Results" in result.stdout
    assert "Evidence graph correlation complete." in result.stdout
    assert "Saved markdown report" in result.stdout
    assert report_path.exists()
    assert (tmp_path / ".sentra" / "firmware_scan.json").exists()
    assert (tmp_path / ".sentra" / "correlation.json").exists()
    assert not (report_path / "report.md").exists()


def test_report_accepts_directory_output(tmp_path: Path) -> None:
    firmware_path = tmp_path / "sample.bin"
    firmware_path.write_text("acme password", encoding="utf-8")
    output_dir = tmp_path / "reports"
    output_dir.mkdir()

    runner.invoke(app, ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)])
    runner.invoke(app, ["correlate", "--workspace", str(tmp_path)])

    result = runner.invoke(
        app,
        ["report", "--workspace", str(tmp_path), "--format", "markdown", "--output", str(output_dir)],
    )

    assert result.exit_code == 0
    assert (output_dir / "report.md").exists()


def test_report_accepts_suffixless_directory_output(tmp_path: Path) -> None:
    firmware_path = tmp_path / "sample.bin"
    firmware_path.write_text("acme password", encoding="utf-8")
    output_dir = tmp_path / "reports"

    runner.invoke(app, ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)])
    runner.invoke(app, ["correlate", "--workspace", str(tmp_path)])

    result = runner.invoke(
        app,
        ["report", "--workspace", str(tmp_path), "--format", "markdown", "--output", str(output_dir)],
    )

    assert result.exit_code == 0
    assert (output_dir / "report.md").exists()


def test_report_supports_html_output(tmp_path: Path) -> None:
    firmware_path = tmp_path / "sample.bin"
    firmware_path.write_text("northwind password", encoding="utf-8")

    runner.invoke(app, ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)])
    runner.invoke(app, ["correlate", "--workspace", str(tmp_path)])
    result = runner.invoke(app, ["report", "--workspace", str(tmp_path), "--format", "html"])

    assert result.exit_code == 0
    assert (tmp_path / ".sentra" / "report.html").exists()
    report_content = (tmp_path / ".sentra" / "report.html").read_text(encoding="utf-8")
    assert "graph-visualization-data" in report_content
    assert "Top Risky Paths" in report_content


def test_status_command_summarizes_workspace(tmp_path: Path) -> None:
    firmware_path = tmp_path / "sample.bin"
    firmware_path.write_text("northwind password", encoding="utf-8")

    runner.invoke(app, ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)])
    runner.invoke(app, ["correlate", "--workspace", str(tmp_path)])

    result = runner.invoke(app, ["status", "--workspace", str(tmp_path)])

    assert result.exit_code == 0
    assert "Workspace Status" in result.stdout
    assert "Status Summary" in result.stdout
    assert str(tmp_path) in result.stdout


def test_status_command_supports_json_output(tmp_path: Path) -> None:
    result = runner.invoke(app, ["status", "--workspace", str(tmp_path), "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    assert payload["workspace"] == str(tmp_path.resolve())
    assert "firmware_extraction_provider" in payload
    assert any(item["name"] == "workspace-inspection" for item in payload["capabilities"])


def test_inspect_workspace_command_lists_artifacts(tmp_path: Path) -> None:
    firmware_path = tmp_path / "sample.bin"
    firmware_path.write_text("acme password", encoding="utf-8")

    runner.invoke(app, ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)])

    result = runner.invoke(app, ["inspect-workspace", "--workspace", str(tmp_path)])

    assert result.exit_code == 0
    assert "Workspace Artifacts" in result.stdout
    assert "firmware_scan" in result.stdout
    assert "Workspace Inspection Summary" in result.stdout


def test_status_and_inspect_workspace_show_firmware_analysis_artifacts(tmp_path: Path) -> None:
    firmware_path = tmp_path / "firmware.zip"
    with zipfile.ZipFile(firmware_path, "w") as archive:
        archive.writestr("etc/device.conf", "password=admin\n")

    runner.invoke(app, ["scan", "firmware", "--path", str(firmware_path), "--workspace", str(tmp_path)])

    status_result = runner.invoke(app, ["status", "--workspace", str(tmp_path)])
    inspect_result = runner.invoke(app, ["inspect-workspace", "--workspace", str(tmp_path)])

    assert status_result.exit_code == 0
    assert "Extracted Files" in status_result.stdout
    assert "Config Hints" in status_result.stdout
    assert "Workspace" in status_result.stdout
    assert inspect_result.exit_code == 0
    assert "firmware_extract_root" in inspect_result.stdout


def test_quick_scan_runs_hero_workflow(tmp_path: Path) -> None:
    result = runner.invoke(
        app,
        ["quick-scan", "--target", "192.168.10.0/24", "--workspace", str(tmp_path), "--yes"],
    )

    assert result.exit_code == 0
    assert "Quick Scan" in result.stdout
    assert "Collected network evidence" in result.stdout
    assert (tmp_path / ".sentra" / "network_scan.json").exists()
    assert (tmp_path / ".sentra" / "correlation.json").exists()
    assert (tmp_path / ".sentra" / "report.html").exists()


def test_global_workspace_flag_is_respected(tmp_path: Path) -> None:
    firmware_path = tmp_path / "sample.bin"
    firmware_path.write_text("acme password", encoding="utf-8")

    result = runner.invoke(
        app,
        ["--workspace", str(tmp_path), "scan", "firmware", "--path", str(firmware_path)],
    )

    assert result.exit_code == 0
    assert (tmp_path / ".sentra" / "firmware_scan.json").exists()


def test_diff_command_writes_markdown_report(tmp_path: Path) -> None:
    before = tmp_path / "before"
    after = tmp_path / "after"
    before.mkdir()
    after.mkdir()
    (before / ".sentra").mkdir()
    (after / ".sentra").mkdir()
    (before / ".sentra" / "network_scan.json").write_text(
        json.dumps({"target": "t", "devices": [{"host": "a", "ip_address": "1.1.1.1", "services": [], "findings": []}]}),
        encoding="utf-8",
    )
    (after / ".sentra" / "network_scan.json").write_text(
        json.dumps({"target": "t", "devices": [{"host": "b", "ip_address": "1.1.1.2", "services": [], "findings": []}]}),
        encoding="utf-8",
    )
    destination = tmp_path / "diffs"
    destination.mkdir()

    result = runner.invoke(
        app,
        ["diff", "--before-workspace", str(before), "--after-workspace", str(after), "--output", str(destination)],
    )

    assert result.exit_code == 0
    assert (destination / "scan-diff.md").exists()


def test_diff_command_supports_html_report(tmp_path: Path) -> None:
    before = tmp_path / "before"
    after = tmp_path / "after"
    before.mkdir()
    after.mkdir()
    (before / ".sentra").mkdir()
    (after / ".sentra").mkdir()
    (before / ".sentra" / "network_scan.json").write_text(
        json.dumps({"target": "t", "devices": [{"host": "a", "ip_address": "1.1.1.1", "services": [], "findings": []}]}),
        encoding="utf-8",
    )
    (after / ".sentra" / "network_scan.json").write_text(
        json.dumps({"target": "t", "devices": [{"host": "b", "ip_address": "1.1.1.2", "services": [], "findings": []}]}),
        encoding="utf-8",
    )
    destination = tmp_path / "diffs"
    destination.mkdir()

    result = runner.invoke(
        app,
        ["diff", "--before-workspace", str(before), "--after-workspace", str(after), "--format", "html", "--output", str(destination)],
    )

    assert result.exit_code == 0
    assert (destination / "scan-diff.html").exists()


def test_scan_firmware_handles_directory_path_with_friendly_error(tmp_path: Path) -> None:
    result = runner.invoke(app, ["scan", "firmware", "--path", str(tmp_path), "--workspace", str(tmp_path)])

    assert result.exit_code == 1


def test_report_handles_invalid_workspace_path(tmp_path: Path) -> None:
    file_workspace = tmp_path / "workspace.txt"
    file_workspace.write_text("not-a-directory", encoding="utf-8")

    result = runner.invoke(app, ["report", "--workspace", str(file_workspace)])

    assert result.exit_code == 1


def test_root_command_handles_invalid_workspace_path(tmp_path: Path) -> None:
    file_workspace = tmp_path / "workspace.txt"
    file_workspace.write_text("not-a-directory", encoding="utf-8")
    env = dict(**os.environ, PYTHONPATH=str(Path(__file__).resolve().parents[1] / "src"))

    result = subprocess.run(
        [sys.executable, "-m", "sentra", "--workspace", str(file_workspace), "report"],
        capture_output=True,
        text=True,
        cwd=Path(__file__).resolve().parents[1],
        env=env,
        check=False,
    )

    assert result.returncode == 1
    assert "Workspace path must be a directory" in result.stderr


def test_diff_rejects_invalid_format(tmp_path: Path) -> None:
    before = tmp_path / "before"
    before.mkdir()
    (before / ".sentra").mkdir()
    (before / ".sentra" / "network_scan.json").write_text(json.dumps({"target": "t", "devices": []}), encoding="utf-8")
    env = dict(**os.environ, PYTHONPATH=str(Path(__file__).resolve().parents[1] / "src"))

    result = subprocess.run(
        [sys.executable, "-m", "sentra", "diff", "--before-workspace", str(before), "--format", "xml"],
        capture_output=True,
        text=True,
        cwd=Path(__file__).resolve().parents[1],
        env=env,
        check=False,
    )

    assert result.returncode == 1
    assert "Diff format must be terminal, markdown, json, or html." in result.stderr


def test_python_module_entrypoint_help_works() -> None:
    env = dict(**os.environ, PYTHONPATH=str(Path(__file__).resolve().parents[1] / "src"))
    result = subprocess.run(
        [sys.executable, "-m", "sentra", "--help"],
        capture_output=True,
        text=True,
        cwd=Path(__file__).resolve().parents[1],
        env=env,
        check=False,
    )

    assert result.returncode == 0
    assert "Sentra is a defensive IoT evidence graph CLI" in result.stdout
    assert "quick-scan" in result.stdout
    assert "Start here:" in result.stdout
    assert "Workspace and intelligence checks:" in result.stdout


def test_fingerprints_validate_command_reports_issues(tmp_path: Path) -> None:
    override_path = tmp_path / "fingerprints.json"
    override_path.write_text(
        json.dumps(
            [
                {
                    "name": "Bad Entry",
                    "vendor": "Unknown",
                    "model": "Unknown",
                    "services": [],
                    "products": [],
                    "metadata_tokens": [],
                    "endpoint_patterns": ["Admin"],
                    "protocol_hints": [],
                    "confidence": 0.2
                }
            ]
        ),
        encoding="utf-8",
    )

    result = runner.invoke(app, ["fingerprints", "validate", "--override", str(override_path)])

    assert result.exit_code == 0
    assert "Validated" in result.stdout
    assert "Bad Entry" in result.stdout


def test_vulns_inspect_command_supports_json(tmp_path: Path) -> None:
    feed_path = tmp_path / ".sentra" / "cve_feed.json"
    feed_path.parent.mkdir(parents=True, exist_ok=True)
    feed_path.write_text(
        json.dumps(
            [
                {
                    "cve_id": "CVE-2026-1111",
                    "product": "goahead",
                    "severity": "high",
                    "summary": "test"
                }
            ]
        ),
        encoding="utf-8",
    )

    result = runner.invoke(app, ["vulns", "inspect", "--workspace", str(tmp_path), "--json"])

    assert result.exit_code == 0
    payload = json.loads(result.stdout)
    assert payload["provider"] == "local-json"
    assert payload["entries"] == 1
    assert payload["products"] == ["goahead"]

