"""Tests for campaign discovery module.

Tests verify:
- discover_campaigns scans filesystem and returns correct CampaignInfo
- Orphaned campaigns (missing/invalid marker) are detected
- Corrupt databases are skipped gracefully
- Results are sorted by updated_at descending
"""

from datetime import datetime

from unfamiliar.campaign.discovery import discover_campaigns
from unfamiliar.campaign.types import CampaignInfo
from unfamiliar.constants import CampaignStatus


def create_campaign_database(
    db_path,
    status: str = "active",
    created_at: str | None = "2024-01-01T10:00:00",
    updated_at: str | None = "2024-01-15T14:30:00",
    missions: list[dict] | None = None,
):
    """Create a valid campaign SQLite database at the given path.

    Args:
        db_path: Path to create the database file.
        status: Campaign status string.
        created_at: ISO 8601 created timestamp.
        updated_at: ISO 8601 updated timestamp.
        missions: List of mission dicts with 'status' key.
    """
    import sqlite3

    conn = sqlite3.connect(db_path)
    conn.execute("PRAGMA foreign_keys = ON")

    # Create minimal schema for campaign and mission tables
    conn.execute("""
        CREATE TABLE campaign (
            id INTEGER PRIMARY KEY,
            repo_path TEXT,
            status TEXT,
            created_at TEXT,
            updated_at TEXT
        )
    """)
    conn.execute("""
        CREATE TABLE mission (
            id INTEGER PRIMARY KEY,
            campaign_id INTEGER,
            title TEXT,
            status TEXT,
            FOREIGN KEY (campaign_id) REFERENCES campaign(id)
        )
    """)

    # Insert campaign record
    conn.execute(
        "INSERT INTO campaign (repo_path, status, created_at, updated_at) "
        "VALUES (?, ?, ?, ?)",
        ("/tmp/test-repo", status, created_at, updated_at),
    )

    # Insert missions if provided
    if missions:
        for mission in missions:
            conn.execute(
                "INSERT INTO mission (campaign_id, title, status) VALUES (?, ?, ?)",
                (1, mission.get("title", "Test Mission"), mission["status"]),
            )
    else:
        # Insert one default mission
        conn.execute(
            "INSERT INTO mission (campaign_id, title, status) VALUES (?, ?, ?)",
            (1, "Test Mission", "available"),
        )

    conn.commit()
    conn.close()


class TestDiscoverCampaignsEmptyDirectory:
    """Tests for empty or missing campaigns directory scenarios."""

    def test_no_campaigns_directory_returns_empty_list(self, tmp_path):
        """Base dir exists but has no campaigns/ subdirectory returns []."""
        result = discover_campaigns(base_dir=tmp_path)
        assert result == []

    def test_empty_campaigns_directory_returns_empty_list(self, tmp_path):
        """Campaigns directory exists but is empty returns []."""
        campaigns_dir = tmp_path / "campaigns"
        campaigns_dir.mkdir()

        result = discover_campaigns(base_dir=tmp_path)
        assert result == []


class TestDiscoverCampaignsSingleCampaign:
    """Tests for discovering a single valid campaign."""

    def test_single_campaign_returns_correct_info(self, tmp_path):
        """One valid campaign dir with marker, DB, and campaign record
        returns correct CampaignInfo.
        """
        # Setup: create campaign directory with marker and database
        campaign_dir = tmp_path / "campaigns" / "test-repo-abc123"
        campaign_dir.mkdir(parents=True)

        # Create marker file pointing to existing repo path
        repo_path = tmp_path / "repos" / "test-repo"
        repo_path.mkdir(parents=True)
        marker_file = campaign_dir / ".repo_path"
        marker_file.write_text(str(repo_path))

        # Create database with specific data
        db_path = campaign_dir / "campaign.db"
        create_campaign_database(
            db_path,
            status="active",
            created_at="2024-03-10T09:00:00",
            updated_at="2024-03-15T16:45:00",
            missions=[
                {"title": "Mission 1", "status": "completed"},
                {"title": "Mission 2", "status": "available"},
                {"title": "Mission 3", "status": "completed"},
            ],
        )

        result = discover_campaigns(base_dir=tmp_path)

        # Assert exact count
        assert len(result) == 1

        # Assert ALL fields on the returned CampaignInfo
        info = result[0]
        assert isinstance(info, CampaignInfo)
        assert info.campaign_dir == campaign_dir
        assert info.repo_path == repo_path
        assert info.status == CampaignStatus.ACTIVE
        assert info.is_orphaned is False
        assert info.created_at == datetime(2024, 3, 10, 9, 0, 0)
        assert info.updated_at == datetime(2024, 3, 15, 16, 45, 0)
        assert info.mission_count == 3
        assert info.completed_count == 2


class TestDiscoverCampaignsSorting:
    """Tests for result sorting by updated_at."""

    def test_multiple_campaigns_sorted_by_updated_at_descending(self, tmp_path):
        """Multiple campaigns with different timestamps are sorted newest first."""
        campaigns_dir = tmp_path / "campaigns"

        # Create three campaigns with different updated_at timestamps
        timestamps = [
            ("oldest", "2024-01-01T10:00:00", "2024-01-10T12:00:00"),
            ("newest", "2024-02-01T10:00:00", "2024-02-20T15:30:00"),
            ("middle", "2024-01-15T10:00:00", "2024-01-25T09:00:00"),
        ]

        for name, created_at, updated_at in timestamps:
            campaign_dir = campaigns_dir / f"campaign-{name}"
            campaign_dir.mkdir(parents=True)

            repo_path = tmp_path / "repos" / name
            repo_path.mkdir(parents=True)
            (campaign_dir / ".repo_path").write_text(str(repo_path))

            db_path = campaign_dir / "campaign.db"
            create_campaign_database(
                db_path, updated_at=updated_at, created_at=created_at
            )

        result = discover_campaigns(base_dir=tmp_path)

        # Assert exact count
        assert len(result) == 3

        # Assert sorted by updated_at descending
        assert result[0].updated_at == datetime(2024, 2, 20, 15, 30, 0)  # newest
        assert result[1].updated_at == datetime(2024, 1, 25, 9, 0, 0)  # middle
        assert result[2].updated_at == datetime(2024, 1, 10, 12, 0, 0)  # oldest


class TestDiscoverCampaignsOrphanedDetection:
    """Tests for orphaned campaign detection."""

    def test_campaign_with_missing_marker_is_orphaned(self, tmp_path):
        """Campaign without .repo_path marker file has is_orphaned=True and
        repo_path=None.
        """
        campaign_dir = tmp_path / "campaigns" / "orphan-no-marker"
        campaign_dir.mkdir(parents=True)

        # Create database but NO marker file
        db_path = campaign_dir / "campaign.db"
        create_campaign_database(db_path)

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        info = result[0]
        assert info.is_orphaned is True
        assert info.repo_path is None

    def test_campaign_with_nonexistent_repo_path_is_orphaned(self, tmp_path):
        """Marker pointing to path that doesn't exist results in is_orphaned=True."""
        campaign_dir = tmp_path / "campaigns" / "orphan-bad-path"
        campaign_dir.mkdir(parents=True)

        # Create marker pointing to non-existent path
        nonexistent_path = tmp_path / "repos" / "does-not-exist"
        (campaign_dir / ".repo_path").write_text(str(nonexistent_path))

        db_path = campaign_dir / "campaign.db"
        create_campaign_database(db_path)

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        info = result[0]
        assert info.is_orphaned is True
        # repo_path is set from marker file content, even if path doesn't exist
        assert info.repo_path == nonexistent_path

    def test_campaign_with_valid_repo_path_not_orphaned(self, tmp_path):
        """Marker pointing to existing path results in is_orphaned=False."""
        campaign_dir = tmp_path / "campaigns" / "valid-campaign"
        campaign_dir.mkdir(parents=True)

        # Create existing repo path and marker
        repo_path = tmp_path / "repos" / "my-project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        db_path = campaign_dir / "campaign.db"
        create_campaign_database(db_path)

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        info = result[0]
        assert info.is_orphaned is False
        assert info.repo_path == repo_path


class TestDiscoverCampaignsCorruptDatabase:
    """Tests for handling corrupt or invalid databases."""

    def test_corrupt_database_is_skipped(self, tmp_path):
        """Campaign dir with invalid/empty DB file is skipped, not in results."""
        campaign_dir = tmp_path / "campaigns" / "corrupt-campaign"
        campaign_dir.mkdir(parents=True)

        # Create marker
        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        # Create invalid database file (not a valid SQLite DB)
        db_path = campaign_dir / "campaign.db"
        db_path.write_text("this is not a valid sqlite database")

        result = discover_campaigns(base_dir=tmp_path)

        # Corrupt database should be skipped, returning empty list
        assert result == []

    def test_database_missing_campaign_table_is_skipped(self, tmp_path):
        """Campaign database without campaign table is skipped."""
        import sqlite3

        campaign_dir = tmp_path / "campaigns" / "no-campaign-table"
        campaign_dir.mkdir(parents=True)

        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        # Create database with wrong schema (no campaign table)
        db_path = campaign_dir / "campaign.db"
        conn = sqlite3.connect(db_path)
        conn.execute("CREATE TABLE wrong_table (id INTEGER PRIMARY KEY)")
        conn.commit()
        conn.close()

        result = discover_campaigns(base_dir=tmp_path)

        assert result == []

    def test_empty_database_file_is_skipped(self, tmp_path):
        """Empty database file (0 bytes) is skipped."""
        campaign_dir = tmp_path / "campaigns" / "empty-db"
        campaign_dir.mkdir(parents=True)

        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        # Create empty file
        db_path = campaign_dir / "campaign.db"
        db_path.write_text("")

        result = discover_campaigns(base_dir=tmp_path)

        assert result == []


class TestDiscoverCampaignsDirectoryWithNonDirEntries:
    """Tests for handling non-directory entries in campaigns folder."""

    def test_non_directory_entries_are_ignored(self, tmp_path):
        """Files in campaigns directory are ignored, only subdirectories are scanned."""
        campaigns_dir = tmp_path / "campaigns"
        campaigns_dir.mkdir()

        # Create a file in campaigns directory (should be ignored)
        (campaigns_dir / "readme.txt").write_text("This is a file, not a campaign")

        # Create a valid campaign subdirectory
        campaign_dir = campaigns_dir / "valid-campaign"
        campaign_dir.mkdir()

        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        db_path = campaign_dir / "campaign.db"
        create_campaign_database(db_path)

        result = discover_campaigns(base_dir=tmp_path)

        # Only the subdirectory campaign should be found
        assert len(result) == 1
        assert result[0].campaign_dir == campaign_dir


class TestDiscoverCampaignsMissingDatabase:
    """Tests for campaign directories without database files."""

    def test_campaign_dir_without_database_is_skipped(self, tmp_path):
        """Campaign directory without campaign.db file is skipped."""
        campaign_dir = tmp_path / "campaigns" / "no-database"
        campaign_dir.mkdir(parents=True)

        # Create marker but no database
        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        result = discover_campaigns(base_dir=tmp_path)

        assert result == []


class TestDiscoverCampaignsMarkerEdgeCases:
    """Tests for edge cases in .repo_path marker file handling."""

    def test_empty_marker_file_is_treated_as_orphaned(self, tmp_path):
        """Marker file with empty content results in is_orphaned=True,
        repo_path=None.
        """
        campaign_dir = tmp_path / "campaigns" / "empty-marker"
        campaign_dir.mkdir(parents=True)

        # Create empty marker file
        (campaign_dir / ".repo_path").write_text("")

        db_path = campaign_dir / "campaign.db"
        create_campaign_database(db_path)

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        info = result[0]
        assert info.is_orphaned is True
        assert info.repo_path is None

    def test_marker_file_with_whitespace_only_is_treated_as_orphaned(self, tmp_path):
        """Marker file with only whitespace results in is_orphaned=True,
        repo_path=None.
        """
        campaign_dir = tmp_path / "campaigns" / "whitespace-marker"
        campaign_dir.mkdir(parents=True)

        # Create marker with only whitespace
        (campaign_dir / ".repo_path").write_text("   \n\t  ")

        db_path = campaign_dir / "campaign.db"
        create_campaign_database(db_path)

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        info = result[0]
        assert info.is_orphaned is True
        assert info.repo_path is None


class TestDiscoverCampaignsStatusParsing:
    """Tests for campaign status parsing edge cases."""

    def test_null_status_defaults_to_initializing(self, tmp_path):
        """Database with NULL status returns CampaignStatus.INITIALIZING."""
        import sqlite3

        campaign_dir = tmp_path / "campaigns" / "null-status"
        campaign_dir.mkdir(parents=True)

        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        # Create database with NULL status
        db_path = campaign_dir / "campaign.db"
        conn = sqlite3.connect(db_path)
        conn.execute("PRAGMA foreign_keys = ON")
        conn.execute("""
            CREATE TABLE campaign (
                id INTEGER PRIMARY KEY,
                repo_path TEXT,
                status TEXT,
                created_at TEXT,
                updated_at TEXT
            )
        """)
        conn.execute("""
            CREATE TABLE mission (
                id INTEGER PRIMARY KEY,
                campaign_id INTEGER,
                title TEXT,
                status TEXT,
                FOREIGN KEY (campaign_id) REFERENCES campaign(id)
            )
        """)
        # Insert campaign with NULL status
        conn.execute(
            "INSERT INTO campaign (repo_path, status, created_at, updated_at) "
            "VALUES (?, ?, ?, ?)",
            ("/tmp/test", None, "2024-01-01T10:00:00", "2024-01-02T10:00:00"),
        )
        conn.execute(
            "INSERT INTO mission (campaign_id, title, status) VALUES (?, ?, ?)",
            (1, "Test", "available"),
        )
        conn.commit()
        conn.close()

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        assert result[0].status == CampaignStatus.INITIALIZING

    def test_unknown_status_string_defaults_to_initializing(self, tmp_path):
        """Database with unrecognized status value returns
        CampaignStatus.INITIALIZING.
        """
        campaign_dir = tmp_path / "campaigns" / "unknown-status"
        campaign_dir.mkdir(parents=True)

        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        db_path = campaign_dir / "campaign.db"
        create_campaign_database(db_path, status="invalid_status_value")

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        assert result[0].status == CampaignStatus.INITIALIZING


class TestDiscoverCampaignsTimestampParsing:
    """Tests for timestamp parsing edge cases."""

    def test_null_timestamps_return_none(self, tmp_path):
        """Database with NULL created_at/updated_at returns None for those fields."""
        import sqlite3

        campaign_dir = tmp_path / "campaigns" / "null-timestamps"
        campaign_dir.mkdir(parents=True)

        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        # Create database with NULL timestamps
        db_path = campaign_dir / "campaign.db"
        conn = sqlite3.connect(db_path)
        conn.execute("PRAGMA foreign_keys = ON")
        conn.execute("""
            CREATE TABLE campaign (
                id INTEGER PRIMARY KEY,
                repo_path TEXT,
                status TEXT,
                created_at TEXT,
                updated_at TEXT
            )
        """)
        conn.execute("""
            CREATE TABLE mission (
                id INTEGER PRIMARY KEY,
                campaign_id INTEGER,
                title TEXT,
                status TEXT,
                FOREIGN KEY (campaign_id) REFERENCES campaign(id)
            )
        """)
        conn.execute(
            "INSERT INTO campaign (repo_path, status, created_at, updated_at) "
            "VALUES (?, ?, ?, ?)",
            ("/tmp/test", "active", None, None),
        )
        conn.execute(
            "INSERT INTO mission (campaign_id, title, status) VALUES (?, ?, ?)",
            (1, "Test", "available"),
        )
        conn.commit()
        conn.close()

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        info = result[0]
        assert info.created_at is None
        assert info.updated_at is None

    def test_invalid_timestamp_format_returns_none(self, tmp_path):
        """Database with invalid datetime string returns None for that field."""
        campaign_dir = tmp_path / "campaigns" / "invalid-timestamp"
        campaign_dir.mkdir(parents=True)

        repo_path = tmp_path / "repos" / "project"
        repo_path.mkdir(parents=True)
        (campaign_dir / ".repo_path").write_text(str(repo_path))

        db_path = campaign_dir / "campaign.db"
        create_campaign_database(
            db_path,
            created_at="not-a-valid-datetime",
            updated_at="also-invalid",
        )

        result = discover_campaigns(base_dir=tmp_path)

        assert len(result) == 1
        info = result[0]
        assert info.created_at is None
        assert info.updated_at is None


class TestDiscoverCampaignsCampaignsPathNotDirectory:
    """Tests for when campaigns path exists but is not a directory."""

    def test_campaigns_path_is_file_returns_empty_list(self, tmp_path):
        """If 'campaigns' exists but is a file, returns empty list."""
        # Create a file named 'campaigns' instead of a directory
        campaigns_file = tmp_path / "campaigns"
        campaigns_file.write_text("I am a file, not a directory")

        result = discover_campaigns(base_dir=tmp_path)

        assert result == []
