"""Integration tests for CLI commands."""

import json
import os

import pytest

from bilgeai.cli.app import app


@pytest.fixture(autouse=True)
def _clean_api_keys(monkeypatch):
    """Ensure API keys are not leaked from .env into test environment."""
    for key in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"):
        monkeypatch.delenv(key, raising=False)


class TestCLI:
    def test_help_output(self, cli_runner):
        result = cli_runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "BilgeAI" in result.output

    def test_version_flag(self, cli_runner):
        result = cli_runner.invoke(app, ["--version"])
        assert result.exit_code == 0
        assert "BilgeAI" in result.output

    def test_profile_command_runs(self, cli_runner):
        result = cli_runner.invoke(app, ["profile", "--no-save"])
        assert result.exit_code == 0
        assert "Hardware Profile" in result.output

    def test_profile_saves_json(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["profile"])
        assert result.exit_code == 0
        profile_path = tmp_path / ".bilge" / "hardware_profile.json"
        assert profile_path.exists()
        data = json.loads(profile_path.read_text())
        assert "cpu_cores" in data
        assert "gpus" in data

    def test_init_creates_config(self, cli_runner, tmp_path):
        # Flow: model, dataset, provider(1=anthropic), llm_model, api_key(masked), pii
        result = cli_runner.invoke(
            app,
            ["init", str(tmp_path)],
            input="meta-llama/Llama-3-8B\n./train.jsonl\n1\nclaude-sonnet-4-20250514\ntest-key\ny\n",
        )
        assert result.exit_code == 0
        assert (tmp_path / "bilge.yaml").exists()
        assert (tmp_path / ".bilge").exists()

    def test_init_overwrite_rejected(self, cli_runner, tmp_path):
        # First init
        cli_runner.invoke(
            app,
            ["init", str(tmp_path)],
            input="test-model\n./data.jsonl\n1\nclaude-sonnet-4-20250514\ntest-key\ny\n",
        )
        # Second init, reject overwrite
        result = cli_runner.invoke(
            app,
            ["init", str(tmp_path)],
            input="n\n",
        )
        assert result.exit_code == 0
        assert "Aborted" in result.output

    def test_analyze_with_dataset(self, cli_runner, sample_alpaca_jsonl, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["analyze", "--data", str(sample_alpaca_jsonl)])
        assert result.exit_code == 0
        assert "Dataset Profile" in result.output
        assert "alpaca" in result.output
        assert "3" in result.output  # 3 rows

    def test_analyze_nonexistent_data(self, cli_runner):
        result = cli_runner.invoke(app, ["analyze", "--data", "/nonexistent/file.jsonl"])
        assert result.exit_code == 1

    def test_analyze_pii_detection(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        # Create dataset with PII
        data_file = tmp_path / "pii_data.jsonl"
        data_file.write_text(
            '{"text": "Contact user@example.com for info"}\n{"text": "Call +90 532 123 45 67"}\n',
            encoding="utf-8",
        )
        result = cli_runner.invoke(app, ["analyze", "--data", str(data_file)])
        assert result.exit_code == 0
        assert "PII Detected" in result.output

    def test_analyze_skip_pii(self, cli_runner, sample_alpaca_jsonl, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(
            app, ["analyze", "--data", str(sample_alpaca_jsonl), "--skip-pii"]
        )
        assert result.exit_code == 0
        assert "PII" not in result.output

    def test_doctor_command_runs(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["doctor"])
        # May fail because no config exists, but should not crash
        assert "Pre-flight Checks" in result.output

    def test_doctor_with_config(self, cli_runner, tmp_path, sample_alpaca_jsonl):
        os.chdir(tmp_path)
        # Init project first
        cli_runner.invoke(
            app,
            ["init", str(tmp_path)],
            input=f"test-model\n{sample_alpaca_jsonl}\n1\nclaude-sonnet-4-20250514\n\ny\n",
        )
        result = cli_runner.invoke(app, ["doctor"])
        assert "Pre-flight Checks" in result.output
        assert "Summary" in result.output

    def test_explain_command_runs(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["explain"])
        assert result.exit_code == 0
        assert "Configuration Decisions" in result.output

    def test_history_empty(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["history"])
        assert result.exit_code == 0
        assert "No runs found" in result.output

    def test_history_with_runs(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        store.create_run(config_dict={"model_name": "test-model"}, model_name="test-model")
        store.create_run(config_dict={"model_name": "test-model-2"}, model_name="test-model-2")

        result = cli_runner.invoke(app, ["history"])
        assert result.exit_code == 0
        assert "Training Runs" in result.output
        assert "showing 2" in result.output

    def test_history_json_output(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        store.create_run(config_dict={"model_name": "test"}, model_name="test")

        result = cli_runner.invoke(app, ["history", "--json"])
        assert result.exit_code == 0
        assert "run_id" in result.output

    def test_diff_command(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        r1 = store.create_run(config_dict={"batch_size": 4}, model_name="m1")
        r2 = store.create_run(config_dict={"batch_size": 8}, model_name="m2")

        result = cli_runner.invoke(app, ["diff", r1.run_id, r2.run_id])
        assert result.exit_code == 0
        assert "Comparing" in result.output

    def test_diff_not_found(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["diff", "fake_a", "fake_b"])
        assert result.exit_code == 1

    def test_export_markdown(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        record = store.create_run(
            config_dict={"model_name": "test", "batch_size": 4},
            model_name="test-model",
        )

        result = cli_runner.invoke(app, ["export", record.run_id])
        assert result.exit_code == 0
        assert "# Training Run" in result.output
        assert "test-model" in result.output

    def test_export_json(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        record = store.create_run(config_dict={"model_name": "test"}, model_name="test")

        result = cli_runner.invoke(app, ["export", record.run_id, "--format", "json"])
        assert result.exit_code == 0
        assert "run_id" in result.output

    def test_export_to_file(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        record = store.create_run(config_dict={"model_name": "test"}, model_name="test")
        out_file = tmp_path / "report.md"

        result = cli_runner.invoke(app, ["export", record.run_id, "-o", str(out_file)])
        assert result.exit_code == 0
        assert out_file.exists()
        assert "# Training Run" in out_file.read_text(encoding="utf-8")

    def test_export_latest_run(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        store.create_run(config_dict={"model_name": "latest"}, model_name="latest-model")

        result = cli_runner.invoke(app, ["export"])
        assert result.exit_code == 0
        assert "latest-model" in result.output

    def test_export_not_found(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["export", "nonexistent_run"])
        assert result.exit_code == 1

    def test_export_latex(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        record = store.create_run(
            config_dict={"model_name": "test", "batch_size": 4},
            model_name="test-model",
        )

        result = cli_runner.invoke(app, ["export", record.run_id, "--format", "latex"])
        assert result.exit_code == 0
        assert "\\begin{table}" in result.output
        assert "\\begin{tabular}" in result.output
        assert "test-model" in result.output

    def test_export_latex_to_file(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        record = store.create_run(config_dict={"model_name": "test"}, model_name="test")
        out_file = tmp_path / "report.tex"

        result = cli_runner.invoke(
            app, ["export", record.run_id, "--format", "latex", "-o", str(out_file)]
        )
        assert result.exit_code == 0
        assert out_file.exists()
        content = out_file.read_text(encoding="utf-8")
        assert "\\begin{table}" in content

    def test_replay_not_found(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["replay", "nonexistent_run"])
        assert result.exit_code == 1

    def test_replay_latest(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()
        store.create_run(
            config_dict={
                "model_name": "test/model",
                "batch_size": 4,
                "quantization": "4bit",
                "learning_rate": 2e-4,
                "num_epochs": 3,
                "max_seq_length": 512,
                "lora_enabled": True,
                "lora_rank": 16,
                "lora_alpha": 32,
                "fp16": True,
                "bf16": False,
                "seed": 42,
            },
            hardware_dict={
                "cpu_cores": 8,
                "cpu_name": "Test CPU",
                "ram_total_gb": 32.0,
                "ram_available_gb": 24.0,
                "disk_free_gb": 100.0,
                "gpus": [{"name": "Test GPU", "vram_gb": 8.0}],
                "num_gpus": 1,
                "os": "Linux",
                "python_version": "3.11",
            },
            model_name="test/model",
        )

        result = cli_runner.invoke(app, ["replay"])
        assert result.exit_code == 0
        assert "Replaying run" in result.output
        assert "New Run ID" in result.output

    def test_chart_no_logs(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        result = cli_runner.invoke(app, ["chart"])
        assert result.exit_code == 1
        assert "No training log found" in result.output

    def test_chart_with_trainer_state(self, cli_runner, tmp_path):
        os.chdir(tmp_path)
        # Create a fake trainer_state.json
        output_dir = tmp_path / ".bilge" / "output"
        output_dir.mkdir(parents=True)
        trainer_state = {
            "log_history": [
                {"loss": 2.5, "step": 10},
                {"loss": 2.0, "step": 20},
                {"loss": 1.5, "step": 30},
                {"loss": 1.2, "step": 40},
            ]
        }
        (output_dir / "trainer_state.json").write_text(json.dumps(trainer_state), encoding="utf-8")

        result = cli_runner.invoke(app, ["chart"])
        assert result.exit_code == 0
        assert "Data points" in result.output

    def test_update_check(self, cli_runner):
        result = cli_runner.invoke(app, ["update", "--check"])
        # Should show current version regardless of network
        assert "0.1.0" in result.output

    def test_update_help(self, cli_runner):
        result = cli_runner.invoke(app, ["update", "--help"])
        assert result.exit_code == 0
        assert "update" in result.output.lower() or "upgrade" in result.output.lower()


class TestFullPipeline:
    """End-to-end tests that exercise the full bilge workflow."""

    def test_init_profile_analyze_doctor_explain_run_flow(
        self, cli_runner, tmp_path, sample_alpaca_jsonl
    ):
        """Test the entire init -> profile -> analyze -> doctor -> explain -> run pipeline."""
        os.chdir(tmp_path)

        # 1. Init (model, dataset, provider=1, llm_model, api_key_env, pii=y)
        result = cli_runner.invoke(
            app,
            ["init", str(tmp_path)],
            input=f"meta-llama/Llama-3-8B\n{sample_alpaca_jsonl}\n1\nclaude-sonnet-4-20250514\n\ny\n",
        )
        assert result.exit_code == 0
        assert (tmp_path / "bilge.yaml").exists()
        assert (tmp_path / ".bilge").is_dir()

        # 2. Profile
        result = cli_runner.invoke(app, ["profile"])
        assert result.exit_code == 0
        assert "Hardware Profile" in result.output
        profile_path = tmp_path / ".bilge" / "hardware_profile.json"
        assert profile_path.exists()

        # 3. Analyze
        result = cli_runner.invoke(app, ["analyze", "--data", str(sample_alpaca_jsonl)])
        assert result.exit_code == 0
        assert "Dataset Profile" in result.output
        assert "alpaca" in result.output

        # 4. Doctor
        result = cli_runner.invoke(app, ["doctor"])
        assert "Pre-flight Checks" in result.output
        assert "PASS" in result.output

        # 5. Explain
        result = cli_runner.invoke(app, ["explain"])
        assert result.exit_code == 0
        assert "Configuration Decisions" in result.output

        # 6. Run (dry-run) - requires API key, should fail gracefully
        result = cli_runner.invoke(app, ["run", "--no-show"])
        # Without API key set, run should exit with error and helpful message
        assert result.exit_code == 1
        assert "API key" in result.output or "bilge init" in result.output

    def test_run_history_diff_export_replay_flow(self, cli_runner, tmp_path):
        """Test run -> history -> diff -> export -> replay pipeline."""
        os.chdir(tmp_path)
        from bilgeai.memory.store import ExperimentStore

        store = ExperimentStore()

        # Create two runs with different configs
        hw_dict = {
            "cpu_cores": 8,
            "cpu_name": "Test CPU",
            "ram_total_gb": 32.0,
            "ram_available_gb": 24.0,
            "disk_free_gb": 100.0,
            "gpus": [{"name": "Test GPU", "vram_gb": 8.0}],
            "num_gpus": 1,
            "os": "Linux",
            "python_version": "3.11",
        }
        r1 = store.create_run(
            config_dict={
                "model_name": "meta-llama/Llama-3-8B",
                "batch_size": 4,
                "quantization": "4bit",
                "learning_rate": 2e-4,
                "num_epochs": 3,
                "max_seq_length": 512,
                "lora_enabled": True,
                "lora_rank": 16,
                "lora_alpha": 32,
                "fp16": False,
                "bf16": True,
                "seed": 42,
            },
            hardware_dict=hw_dict,
            model_name="meta-llama/Llama-3-8B",
        )
        r2 = store.create_run(
            config_dict={
                "model_name": "meta-llama/Llama-3-8B",
                "batch_size": 8,
                "quantization": "8bit",
                "learning_rate": 1e-4,
                "num_epochs": 5,
                "max_seq_length": 1024,
                "lora_enabled": True,
                "lora_rank": 32,
                "lora_alpha": 64,
                "fp16": True,
                "bf16": False,
                "seed": 123,
            },
            hardware_dict=hw_dict,
            model_name="meta-llama/Llama-3-8B",
        )

        # History
        result = cli_runner.invoke(app, ["history"])
        assert result.exit_code == 0
        assert "showing 2" in result.output

        # History --json
        result = cli_runner.invoke(app, ["history", "--json"])
        assert result.exit_code == 0
        assert "run_id" in result.output

        # Diff
        result = cli_runner.invoke(app, ["diff", r1.run_id, r2.run_id])
        assert result.exit_code == 0
        assert "Comparing" in result.output
        assert "batch_size" in result.output

        # Export markdown
        result = cli_runner.invoke(app, ["export", r1.run_id, "--format", "markdown"])
        assert result.exit_code == 0
        assert "# Training Run" in result.output

        # Export json
        result = cli_runner.invoke(app, ["export", r1.run_id, "--format", "json"])
        assert result.exit_code == 0
        assert "run_id" in result.output
        assert r1.run_id in result.output

        # Export latex
        result = cli_runner.invoke(app, ["export", r1.run_id, "--format", "latex"])
        assert result.exit_code == 0
        assert "\\begin{table}" in result.output

        # Export to file
        out_file = tmp_path / "report.md"
        result = cli_runner.invoke(app, ["export", r1.run_id, "-o", str(out_file)])
        assert result.exit_code == 0
        assert out_file.exists()

        # Replay
        result = cli_runner.invoke(app, ["replay", r1.run_id])
        assert result.exit_code == 0
        assert "Replaying run" in result.output
        assert "New Run ID" in result.output

        # Verify replay created a third run
        result = cli_runner.invoke(app, ["history"])
        assert "showing 3" in result.output

    def test_chart_ascii_with_mock_logs(self, cli_runner, tmp_path):
        """Test chart command with mock trainer_state.json."""
        os.chdir(tmp_path)
        output_dir = tmp_path / ".bilge" / "output"
        output_dir.mkdir(parents=True)
        trainer_state = {
            "log_history": [
                {"loss": 3.0, "step": 10},
                {"loss": 2.5, "step": 20},
                {"loss": 2.0, "step": 30},
                {"loss": 1.5, "step": 40},
                {"loss": 1.0, "step": 50},
                {"loss": 0.8, "step": 60},
                {"loss": 0.6, "step": 70},
                {"loss": 0.5, "step": 80},
            ]
        }
        (output_dir / "trainer_state.json").write_text(json.dumps(trainer_state), encoding="utf-8")

        result = cli_runner.invoke(app, ["chart"])
        assert result.exit_code == 0
        assert "Data points: 8" in result.output
        assert "Min value" in result.output
        assert "Max value" in result.output

        # Test with different metric (learning_rate key)
        trainer_state_lr = {
            "log_history": [
                {"learning_rate": 0.0002, "step": 10},
                {"learning_rate": 0.00018, "step": 20},
                {"learning_rate": 0.00015, "step": 30},
            ]
        }
        (output_dir / "trainer_state.json").write_text(
            json.dumps(trainer_state_lr), encoding="utf-8"
        )
        result = cli_runner.invoke(app, ["chart", "--metric", "learning_rate"])
        assert result.exit_code == 0
        assert "Data points: 3" in result.output

    def test_all_12_commands_listed(self, cli_runner):
        """Verify all 12 commands are registered."""
        result = cli_runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        expected_commands = [
            "init",
            "profile",
            "analyze",
            "doctor",
            "explain",
            "run",
            "history",
            "diff",
            "export",
            "replay",
            "chart",
            "update",
        ]
        for cmd in expected_commands:
            assert cmd in result.output, f"Command '{cmd}' not found in help output"

    def test_run_without_api_key_fails_gracefully(self, cli_runner, tmp_path, sample_alpaca_jsonl):
        """Test bilge run without API key exits with helpful message."""
        os.chdir(tmp_path)
        cli_runner.invoke(
            app,
            ["init", str(tmp_path)],
            input=f"meta-llama/Llama-3-8B\n{sample_alpaca_jsonl}\n1\nclaude-sonnet-4-20250514\n\ny\n",
        )
        cli_runner.invoke(app, ["profile"])

        result = cli_runner.invoke(app, ["run", "--no-show"])
        assert result.exit_code == 1
        assert "API key" in result.output

    def test_analyze_detects_format_and_stats(self, cli_runner, tmp_path):
        """Test analyze with different dataset formats."""
        os.chdir(tmp_path)

        # CSV format
        csv_file = tmp_path / "data.csv"
        csv_file.write_text(
            "text\nHello world\nFoo bar baz\nTest sentence here\n",
            encoding="utf-8",
        )
        result = cli_runner.invoke(app, ["analyze", "--data", str(csv_file)])
        assert result.exit_code == 0
        assert "3" in result.output  # 3 rows

    def test_pii_detection_integration(self, cli_runner, tmp_path):
        """Test PII detection with various PII types."""
        os.chdir(tmp_path)
        data_file = tmp_path / "pii_data.jsonl"
        data_file.write_text(
            '{"text": "Email: user@example.com"}\n'
            '{"text": "Phone: +90 532 123 45 67"}\n'
            '{"text": "TC: 12345678901"}\n'
            '{"text": "No PII here, just normal text"}\n',
            encoding="utf-8",
        )
        result = cli_runner.invoke(app, ["analyze", "--data", str(data_file)])
        assert result.exit_code == 0
        assert "PII Detected" in result.output
