"""Tests for the CLI entry point (main.py).

These exercise argument parsing and the full main() flow with mocked
collectors so no real API calls are made.  They are intentionally
cross-platform: nothing here relies on a specific OS, so the CI matrix
can run them on Linux, macOS and Windows.
"""

import json
from pathlib import Path
from unittest import mock

import pytest

from astrix_openclaw_scanner.main import main, parse_args, save_raw_events
from tests.conftest import CRWD_OPENCLAW_EVENT, DEFENDER_OPENCLAW_EVENT, to_json_list

# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------


class TestParseArgs:
    """Verify CLI argument parsing and validation."""

    def test_crowdstrike_minimal(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.setattr(
            "sys.argv",
            ["prog", "crowdstrike", "--client-id", "id", "--client-secret", "secret"],
        )
        args = parse_args()
        assert args.edr == "crowdstrike"
        assert args.client_id == "id"
        assert args.client_secret == "secret"
        assert args.days_back == 14
        assert args.no_filter is False
        assert args.save_raw_events is None

    def test_defender_requires_tenant_id(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.setattr(
            "sys.argv",
            ["prog", "microsoft_defender", "--client-id", "id", "--client-secret", "secret"],
        )
        with pytest.raises(SystemExit):
            parse_args()

    def test_defender_with_tenant_id(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.setattr(
            "sys.argv",
            [
                "prog",
                "microsoft_defender",
                "--client-id",
                "id",
                "--client-secret",
                "secret",
                "--tenant-id",
                "tenant",
            ],
        )
        args = parse_args()
        assert args.edr == "microsoft_defender"
        assert args.tenant_id == "tenant"

    def test_all_flags(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
        out_file = tmp_path / "events.json"
        monkeypatch.setattr(
            "sys.argv",
            [
                "prog",
                "crowdstrike",
                "--client-id",
                "id",
                "--client-secret",
                "secret",
                "--days-back",
                "7",
                "--no-filter",
                "--save-raw-events",
                str(out_file),
            ],
        )
        args = parse_args()
        assert args.days_back == 7
        assert args.no_filter is True
        assert args.save_raw_events == out_file

    def test_invalid_edr_rejected(self, monkeypatch: pytest.MonkeyPatch):
        monkeypatch.setattr(
            "sys.argv",
            ["prog", "splunk", "--client-id", "id", "--client-secret", "secret"],
        )
        with pytest.raises(SystemExit):
            parse_args()


# ---------------------------------------------------------------------------
# save_raw_events
# ---------------------------------------------------------------------------


class TestSaveRawEvents:
    """Verify raw event serialisation."""

    def test_writes_json(self, tmp_path: Path):
        events = to_json_list(CRWD_OPENCLAW_EVENT)
        out = tmp_path / "sub" / "raw.json"
        save_raw_events(events, out)

        assert out.exists()
        data = json.loads(out.read_text())
        assert data["event_count"] == 1
        assert len(data["events"]) == 1
        assert data["events"][0]["ComputerName"] == "WS-PC01"


# ---------------------------------------------------------------------------
# End-to-end main() with mocked collectors
# ---------------------------------------------------------------------------


class TestMainCrowdStrike:
    """Run main() for the CrowdStrike path with a mocked collector."""

    @pytest.fixture(autouse=True)
    def _patch(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
        monkeypatch.setattr(
            "sys.argv",
            ["prog", "crowdstrike", "--client-id", "id", "--client-secret", "secret"],
        )
        monkeypatch.chdir(tmp_path)

    @mock.patch("astrix_openclaw_scanner.main.collect_crowdstrike")
    @mock.patch("webbrowser.open")
    def test_generates_report(self, mock_browser: mock.Mock, mock_collect: mock.Mock, tmp_path: Path):
        mock_collect.return_value = to_json_list(CRWD_OPENCLAW_EVENT)
        main()

        # Report should have been generated under cwd (openclaw_discovery/edr/report_*)
        reports = list((tmp_path / "astrix_openclaw_scanner_results" / "crowdstrike").glob("report_*/index.html"))
        assert len(reports) == 1
        mock_browser.assert_called_once()

    @mock.patch("astrix_openclaw_scanner.main.collect_crowdstrike")
    @mock.patch("webbrowser.open")
    def test_report_path_uses_custom_dir(
        self, mock_browser: mock.Mock, mock_collect: mock.Mock, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ):
        custom_report_dir = tmp_path / "my_reports"
        monkeypatch.setattr(
            "sys.argv",
            [
                "prog",
                "crowdstrike",
                "--client-id",
                "id",
                "--client-secret",
                "secret",
                "--report-path",
                str(custom_report_dir),
            ],
        )
        mock_collect.return_value = to_json_list(CRWD_OPENCLAW_EVENT)
        main()

        assert (custom_report_dir / "index.html").exists()
        mock_browser.assert_called_once()

    @mock.patch("astrix_openclaw_scanner.main.collect_crowdstrike")
    @mock.patch("webbrowser.open")
    def test_no_report_on_empty(self, mock_browser: mock.Mock, mock_collect: mock.Mock):
        mock_collect.return_value = []
        main()
        mock_browser.assert_not_called()

    @mock.patch("astrix_openclaw_scanner.main.collect_crowdstrike")
    @mock.patch("webbrowser.open")
    def test_save_raw_events_flag(
        self, mock_browser: mock.Mock, mock_collect: mock.Mock, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ):
        raw_path = tmp_path / "my_events.json"
        monkeypatch.setattr(
            "sys.argv",
            [
                "prog",
                "crowdstrike",
                "--client-id",
                "id",
                "--client-secret",
                "secret",
                "--save-raw-events",
                str(raw_path),
            ],
        )
        mock_collect.return_value = to_json_list(CRWD_OPENCLAW_EVENT)
        main()

        assert raw_path.exists()
        data = json.loads(raw_path.read_text())
        assert data["event_count"] == 1

    @mock.patch("astrix_openclaw_scanner.main.collect_crowdstrike")
    @mock.patch("webbrowser.open")
    def test_no_raw_events_by_default(
        self, mock_browser: mock.Mock, mock_collect: mock.Mock, tmp_path: Path
    ):
        mock_collect.return_value = to_json_list(CRWD_OPENCLAW_EVENT)
        main()

        # No --save-raw-events path: no raw event file created
        raw_files = list(tmp_path.glob("**/raw_events_*.json")) + list(tmp_path.glob("**/*.json"))
        assert len(raw_files) == 0


class TestMainDefender:
    """Run main() for the Defender path with a mocked collector."""

    @pytest.fixture(autouse=True)
    def _patch(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
        monkeypatch.setattr(
            "sys.argv",
            [
                "prog",
                "microsoft_defender",
                "--client-id",
                "id",
                "--client-secret",
                "secret",
                "--tenant-id",
                "tenant",
            ],
        )
        monkeypatch.chdir(tmp_path)

    @mock.patch("astrix_openclaw_scanner.main.collect_defender")
    @mock.patch("webbrowser.open")
    def test_generates_report(self, mock_browser: mock.Mock, mock_collect: mock.Mock, tmp_path: Path):
        mock_collect.return_value = to_json_list(DEFENDER_OPENCLAW_EVENT)
        main()

        results_dir = tmp_path / "astrix_openclaw_scanner_results" / "microsoft_defender"
        reports = list(results_dir.glob("report_*/index.html"))
        assert len(reports) == 1
        mock_browser.assert_called_once()

    @mock.patch("astrix_openclaw_scanner.main.collect_defender")
    @mock.patch("webbrowser.open")
    def test_no_report_on_empty(self, mock_browser: mock.Mock, mock_collect: mock.Mock):
        mock_collect.return_value = []
        main()
        mock_browser.assert_not_called()

    @mock.patch("astrix_openclaw_scanner.main.collect_defender")
    @mock.patch("webbrowser.open")
    def test_no_filter_flag_passes_through(
        self, mock_browser: mock.Mock, mock_collect: mock.Mock, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ):
        """Verify --no-filter keeps noise events that would normally be removed."""
        monkeypatch.setattr(
            "sys.argv",
            [
                "prog",
                "microsoft_defender",
                "--client-id",
                "id",
                "--client-secret",
                "secret",
                "--tenant-id",
                "tenant",
                "--no-filter",
            ],
        )
        from tests.conftest import DEFENDER_CHROME_NOISE_EVENT

        mock_collect.return_value = to_json_list(DEFENDER_OPENCLAW_EVENT, DEFENDER_CHROME_NOISE_EVENT)
        main()

        # With --no-filter, both events (same endpoint+user) merge into 1 row,
        # but the noise event is NOT dropped beforehand.
        results_dir = tmp_path / "astrix_openclaw_scanner_results" / "microsoft_defender"
        reports = list(results_dir.glob("report_*/index.html"))
        assert len(reports) == 1
