"""Tests for watchlist.py command functions."""

import json
import sqlite3
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

import store
import watchlist
from lib import schema

@pytest.fixture


def temp_db():
    """Create a temporary database for testing."""
    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
        db_path = Path(f.name)
    
    # Override the database path
    original_override = store._db_override
    store._db_override = db_path
    
    # Initialize fresh database
    store.init_db()
    
    yield db_path
    
    # Cleanup
    store._db_override = original_override
    if db_path.exists():
        db_path.unlink()

# === Tests for cmd_add() ===


def test_cmd_add_basic(temp_db, capsys):
    """Test adding a topic with default schedule."""
    args = Mock()
    args.topic = "Test Topic"
    args.weekly = False
    args.schedule = None
    args.queries = None
    
    watchlist.cmd_add(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert output["action"] == "added"
    assert output["topic"] == "Test Topic"
    assert "daily" in output["schedule"]


def test_cmd_add_with_custom_schedule(temp_db, capsys):
    """Test adding a topic with custom schedule."""
    args = Mock()
    args.topic = "Test Topic"
    args.weekly = False
    args.schedule = "0 12 * * *"
    args.queries = None
    
    watchlist.cmd_add(args)
    
    # Verify in database
    topic = store.get_topic("Test Topic")
    assert topic["schedule"] == "0 12 * * *"


def test_cmd_add_weekly(temp_db, capsys):
    """Test adding a topic with weekly schedule."""
    args = Mock()
    args.topic = "Test Topic"
    args.weekly = True
    args.schedule = None
    args.queries = None
    
    watchlist.cmd_add(args)
    
    # Verify weekly schedule
    topic = store.get_topic("Test Topic")
    assert topic["schedule"] == "0 8 * * 1"  # Monday 8am


def test_cmd_add_with_search_queries(temp_db, capsys):
    """Test adding a topic with custom search queries."""
    args = Mock()
    args.topic = "Test Topic"
    args.weekly = False
    args.schedule = None
    args.queries = "query1, query2, query3"
    
    watchlist.cmd_add(args)
    
    # Verify queries stored
    topic = store.get_topic("Test Topic")
    queries = json.loads(topic["search_queries"])
    assert queries == ["query1", "query2", "query3"]

# === Tests for cmd_remove() ===


def test_cmd_remove_existing_topic(temp_db, capsys):
    """Test removing an existing topic."""
    # Add a topic first
    store.add_topic("Test Topic")
    
    args = Mock()
    args.topic = "Test Topic"
    
    watchlist.cmd_remove(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert output["action"] == "removed"
    assert output["topic"] == "Test Topic"


def test_cmd_remove_nonexistent_topic(temp_db, capsys):
    """Test removing a topic that doesn't exist."""
    args = Mock()
    args.topic = "Nonexistent Topic"
    
    watchlist.cmd_remove(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert output["action"] == "not_found"
    assert output["topic"] == "Nonexistent Topic"

# === Tests for cmd_list() ===


def test_cmd_list_empty(temp_db, capsys):
    """Test listing when no topics exist."""
    args = Mock()
    
    watchlist.cmd_list(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert output["topics"] == []
    assert output["budget_used"] == 0.0
    assert output["budget_limit"] == 5.0


def test_cmd_list_with_topics(temp_db, capsys):
    """Test listing with multiple topics."""
    # Add topics
    store.add_topic("Topic 1")
    store.add_topic("Topic 2")
    store.add_topic("Topic 3")
    
    args = Mock()
    
    watchlist.cmd_list(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert len(output["topics"]) == 3
    topic_names = {t["name"] for t in output["topics"]}
    assert topic_names == {"Topic 1", "Topic 2", "Topic 3"}

# === Tests for cmd_delta() ===


def test_cmd_delta_outputs_topic_delta(temp_db, capsys):
    """Test printing the latest watchlist delta as JSON."""
    topic = store.add_topic("Test Topic")
    previous_run_id = store.record_run(topic["id"], source_mode="v3", status="completed")
    store.store_findings(previous_run_id, topic["id"], [
        {
            "source": "reddit",
            "source_url": "https://reddit.com/continued",
            "source_title": "Continued",
            "content": "Still present",
        }
    ])
    current_run_id = store.record_run(topic["id"], source_mode="v3", status="completed")
    store.store_findings(current_run_id, topic["id"], [
        {
            "source": "reddit",
            "source_url": "https://reddit.com/continued",
            "source_title": "Continued",
            "content": "Still present",
        },
        {
            "source": "github",
            "source_url": "https://github.com/example/new",
            "source_title": "New",
            "content": "New this run",
        },
    ])

    args = Mock()
    args.topic = "Test Topic"

    watchlist.cmd_delta(args)

    captured = capsys.readouterr()
    output = json.loads(captured.out)

    assert output["topic"] == "Test Topic"
    assert output["status"] == "ok"
    assert output["current_run_id"] == current_run_id
    assert output["previous_run_id"] == previous_run_id
    assert output["new"] == 1
    assert output["continued"] == 1


def test_cmd_delta_unknown_topic_exits(temp_db):
    """Test delta for an unknown topic exits with an error."""
    args = Mock()
    args.topic = "Missing Topic"

    with pytest.raises(SystemExit):
        watchlist.cmd_delta(args)

# === Tests for cmd_config() ===


def test_cmd_config_delivery(temp_db, capsys):
    """Test configuring delivery channel."""
    args = Mock()
    args.key = "delivery"
    args.value = "https://hooks.slack.com/services/TEST"
    
    watchlist.cmd_config(args)
    
    # Verify setting stored
    channel = store.get_setting("delivery_channel")
    assert channel == "https://hooks.slack.com/services/TEST"
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert output["action"] == "config"
    assert output["key"] == "delivery_channel"


def test_cmd_config_delivery_rejects_non_https(temp_db):
    """A non-https delivery channel is rejected at write time, not stored."""
    args = Mock()
    args.key = "delivery"
    args.value = "http://evil.example/hooks.slack.com"

    with pytest.raises(SystemExit):
        watchlist.cmd_config(args)

    assert not store.get_setting("delivery_channel")


def test_cmd_config_budget(temp_db, capsys):
    """Test configuring daily budget."""
    args = Mock()
    args.key = "budget"
    args.value = 10.0
    
    watchlist.cmd_config(args)
    
    # Verify setting stored
    budget = store.get_setting("daily_budget")
    assert budget == "10.0"


def test_cmd_config_unknown_key(temp_db):
    """Test that unknown config key raises error."""
    args = Mock()
    args.key = "unknown_key"
    args.value = "value"
    
    with pytest.raises(SystemExit):
        watchlist.cmd_config(args)

# === Tests for _run_topic() ===

@patch('watchlist.subprocess.run')


def test_run_topic_success(mock_subprocess, temp_db):
    """Test successful topic run."""
    topic = store.add_topic("Test Topic")
    
    # Mock successful subprocess call
    mock_result = Mock()
    mock_result.returncode = 0
    mock_result.stdout = json.dumps({
        "topic": "Test Topic",
        "range_from": "2026-01-01",
        "range_to": "2026-04-03",
        "generated_at": "2026-04-03T00:00:00Z",
        "provider_runtime": {
            "reasoning_provider": "gemini",
            "planner_model": "gemini-2.0-flash-exp",
            "rerank_model": "gemini-2.0-flash-exp",
        },
        "query_plan": {
            "intent": "test",
            "freshness_mode": "recent",
            "cluster_mode": "standard",
            "raw_topic": "test",
            "subqueries": [],
            "source_weights": {},
        },
        "clusters": [],
        "ranked_candidates": [
            {
                "candidate_id": "c-r1",
                "item_id": "R1",
                "source": "reddit",
                "title": "Test",
                "url": "https://reddit.com/1",
                "snippet": "Snippet",
                "subquery_labels": ["primary"],
                "native_ranks": {"reddit": 1},
                "local_relevance": 0.8,
                "freshness": 100,
                "engagement": 50.0,
                "source_quality": 0.8,
                "rrf_score": 1.0,
                "final_score": 0.8,
                "explanation": "Snippet",
                "source_items": [
                    {
                        "item_id": "R1",
                        "source": "reddit",
                        "title": "Test",
                        "body": "Content",
                        "url": "https://reddit.com/1",
                        "author": "user",
                        "engagement_score": 50.0,
                        "local_relevance": 0.8,
                        "snippet": "Snippet",
                    }
                ],
            }
        ],
        "items_by_source": {
            "reddit": [
                {
                    "item_id": "R1",
                    "source": "reddit",
                    "title": "Test",
                    "body": "Content",
                    "url": "https://reddit.com/1",
                    "author": "user",
                    "engagement_score": 50.0,
                    "local_relevance": 0.8,
                    "snippet": "Snippet",
                }
            ],
        },
        "errors_by_source": {},
        "warnings": [],
    })
    mock_subprocess.return_value = mock_result
    
    result = watchlist._run_topic(topic)
    
    assert result["status"] == "completed"
    assert result["new"] == 1
    assert result["topic"] == "Test Topic"

@patch('watchlist.subprocess.run')


def test_run_topic_failure(mock_subprocess, temp_db):
    """Test topic run failure."""
    topic = store.add_topic("Test Topic")
    
    # Mock failed subprocess call
    mock_result = Mock()
    mock_result.returncode = 1
    mock_result.stderr = "Error message"
    mock_subprocess.return_value = mock_result
    
    result = watchlist._run_topic(topic)
    
    assert result["status"] == "failed"
    assert "Error message" in result["error"]

@patch('watchlist.subprocess.run')


def test_run_topic_timeout(mock_subprocess, temp_db):
    """Test topic run timeout."""
    topic = store.add_topic("Test Topic")
    
    # Mock timeout
    mock_subprocess.side_effect = subprocess.TimeoutExpired("cmd", 300)
    
    result = watchlist._run_topic(topic)
    
    assert result["status"] == "failed"
    assert result["error"] == "timeout"

@patch('watchlist.subprocess.run')
@patch('watchlist._deliver_findings')


def test_run_topic_calls_delivery(mock_deliver, mock_subprocess, temp_db):
    """Test that successful run calls delivery."""
    topic = store.add_topic("Test Topic")
    
    # Mock successful subprocess call with findings
    mock_result = Mock()
    mock_result.returncode = 0
    mock_result.stdout = json.dumps({
        "topic": "Test Topic",
        "range_from": "2026-01-01",
        "range_to": "2026-04-03",
        "generated_at": "2026-04-03T00:00:00Z",
        "provider_runtime": {
            "reasoning_provider": "gemini",
            "planner_model": "gemini-2.0-flash-exp",
            "rerank_model": "gemini-2.0-flash-exp",
        },
        "query_plan": {
            "intent": "test",
            "freshness_mode": "recent",
            "cluster_mode": "standard",
            "raw_topic": "test",
            "subqueries": [],
            "source_weights": {},
        },
        "clusters": [],
        "ranked_candidates": [
            {
                "candidate_id": "c-r1",
                "item_id": "R1",
                "source": "reddit",
                "title": "Test",
                "url": "https://reddit.com/1",
                "snippet": "Snippet",
                "subquery_labels": ["primary"],
                "native_ranks": {"reddit": 1},
                "local_relevance": 0.8,
                "freshness": 100,
                "engagement": 50.0,
                "source_quality": 0.8,
                "rrf_score": 1.0,
                "final_score": 0.8,
                "explanation": "Snippet",
                "source_items": [
                    {
                        "item_id": "R1",
                        "source": "reddit",
                        "title": "Test",
                        "body": "Content",
                        "url": "https://reddit.com/1",
                        "author": "user",
                        "engagement_score": 50.0,
                        "local_relevance": 0.8,
                        "snippet": "Snippet",
                    }
                ],
            }
        ],
        "items_by_source": {
            "reddit": [
                {
                    "item_id": "R1",
                    "source": "reddit",
                    "title": "Test",
                    "body": "Content",
                    "url": "https://reddit.com/1",
                    "author": "user",
                    "engagement_score": 50.0,
                    "local_relevance": 0.8,
                    "snippet": "Snippet",
                }
            ],
        },
        "errors_by_source": {},
        "warnings": [],
    })
    mock_subprocess.return_value = mock_result
    
    watchlist._run_topic(topic)
    
    # Verify delivery was called
    assert mock_deliver.called
    call_args = mock_deliver.call_args[0]
    assert call_args[0] == "Test Topic"
    assert call_args[1]["new"] == 1

# === Tests for cmd_run_one() ===

@patch('watchlist._run_topic')


def test_cmd_run_one(mock_run, temp_db, capsys):
    """Test running a single topic."""
    topic = store.add_topic("Test Topic")
    
    mock_run.return_value = {
        "topic": "Test Topic",
        "status": "completed",
        "new": 5,
        "updated": 2,
        "duration": 60.0,
    }
    
    args = Mock()
    args.topic = "Test Topic"
    
    watchlist.cmd_run_one(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert output["status"] == "completed"
    assert output["new"] == 5


def test_cmd_run_one_nonexistent_topic(temp_db, capsys):
    """Test running a nonexistent topic."""
    args = Mock()
    args.topic = "Nonexistent Topic"
    
    with pytest.raises(SystemExit):
        watchlist.cmd_run_one(args)

# === Tests for cmd_run_all() ===

@patch('watchlist._run_topic')


def test_cmd_run_all_no_topics(mock_run, temp_db, capsys):
    """Test running all topics when none exist."""
    args = Mock()
    
    watchlist.cmd_run_all(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert "No enabled topics" in output["message"]

@patch('watchlist._run_topic')


def test_cmd_run_all_multiple_topics(mock_run, temp_db, capsys):
    """Test running multiple topics."""
    # Add topics
    store.add_topic("Topic 1")
    store.add_topic("Topic 2")
    
    mock_run.return_value = {
        "topic": "Test",
        "status": "completed",
        "new": 5,
        "updated": 2,
        "duration": 60.0,
    }
    
    args = Mock()
    
    watchlist.cmd_run_all(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    assert output["action"] == "run_all"
    assert len(output["results"]) == 2

@patch('watchlist._run_topic')
@patch('watchlist.store.get_daily_cost')


def test_cmd_run_all_respects_budget(mock_cost, mock_run, temp_db, capsys):
    """Test that run-all respects daily budget."""
    # Add topics
    store.add_topic("Topic 1")
    store.add_topic("Topic 2")
    store.add_topic("Topic 3")
    
    # Mock budget exceeded (budget limit is 5.0)
    mock_cost.return_value = 6.0  # Over budget
    
    mock_run.return_value = {
        "topic": "Test",
        "status": "completed",
        "new": 5,
        "updated": 2,
        "duration": 60.0,
    }
    
    args = Mock()
    
    watchlist.cmd_run_all(args)
    
    # Verify output
    captured = capsys.readouterr()
    output = json.loads(captured.out)
    
    # All topics should be skipped due to budget
    results = output["results"]
    skipped = [r for r in results if r["status"] == "skipped"]
    
    assert len(skipped) == 3  # All 3 topics skipped

if __name__ == "__main__":
    pytest.main([__file__, "-v"])
