"""Tests for scoring, filtering, and ranking."""

from __future__ import annotations

from typing import Any

import pytest

from sb_scout.models import (
    AppConfig,
    BonusConfig,
    DiskRequirements,
    FilterConfig,
    ScoreWeights,
)
from sb_scout.scoring import (
    apply_scores,
    build_row,
    build_rows,
    disk_breakdown,
    feature_flags,
    server_matches,
    sort_rows,
    weighted_geomean,
)


class TestDiskBreakdown:
    def test_full_data(self, sample_server):
        d = disk_breakdown(sample_server)
        assert d["nvme_count"] == 2
        assert d["nvme_gb"] == 1024
        assert d["hdd_count"] == 2
        assert d["hdd_gb"] == 8000
        assert d["ssd_count"] == 2
        assert d["storage_gb"] == 9024

    def test_empty_disk_data(self):
        d = disk_breakdown({"serverDiskData": {}})
        assert d["storage_gb"] == 0
        assert d["ssd_count"] == 0

    def test_missing_disk_data(self):
        d = disk_breakdown({})
        assert d["storage_gb"] == 0

    def test_none_disk_data(self):
        d = disk_breakdown({"serverDiskData": None})
        assert d["storage_gb"] == 0

    def test_sata_only(self):
        d = disk_breakdown({"serverDiskData": {"nvme": [], "sata": [256, 256], "hdd": []}})
        assert d["sata_count"] == 2
        assert d["ssd_count"] == 2
        assert d["ssd_gb"] == 512


class TestFeatureFlags:
    def test_inic_in_specials(self):
        has_inic, has_gpu = feature_flags({"specials": ["iNIC"], "description": []}, ("GPU",))
        assert has_inic is True
        assert has_gpu is False

    def test_gpu_in_description(self):
        has_inic, has_gpu = feature_flags(
            {"specials": [], "description": ["NVIDIA RTX 3060"]},
            ("GPU", "NVIDIA", "RTX"),
        )
        assert has_inic is False
        assert has_gpu is True

    def test_gpu_in_specials(self):
        _, has_gpu = feature_flags({"specials": ["GPU"], "description": []}, ("GPU",))
        assert has_gpu is True

    def test_no_features(self):
        has_inic, has_gpu = feature_flags({"specials": [], "description": []}, ("GPU",))
        assert has_inic is False
        assert has_gpu is False


class TestWeightedGeomean:
    def test_single_value(self):
        assert abs(weighted_geomean([(2.0, 1.0)]) - 2.0) < 1e-9

    def test_equal_weights(self):
        # geomean of 4 and 16 = 8
        assert abs(weighted_geomean([(4.0, 1.0), (16.0, 1.0)]) - 8.0) < 1e-9

    def test_zero_value(self):
        assert weighted_geomean([(0.0, 1.0), (10.0, 1.0)]) == 0.0

    def test_zero_weight_ignored(self):
        assert abs(weighted_geomean([(5.0, 1.0), (99.0, 0.0)]) - 5.0) < 1e-9

    def test_empty(self):
        assert weighted_geomean([]) == 0.0


class TestServerMatches:
    def _filters(self, **kw) -> FilterConfig:
        return FilterConfig(
            disk=kw.pop("disk", DiskRequirements()),
            **kw,
        )

    def test_ecc_filter(self, sample_server):
        assert server_matches(
            server=sample_server, cpu="AMD Ryzen 9 5950X", threads=32,
            price_amount=65.0, ram_gb=128, disks=disk_breakdown(sample_server),
            has_gpu=False, filters=self._filters(ecc_only=True),
        ) is True

    def test_ecc_rejects_non_ecc(self):
        srv = {"is_ecc": False, "datacenter": "X"}
        assert server_matches(
            server=srv, cpu="X", threads=8, price_amount=50, ram_gb=32,
            disks={"storage_gb": 0, "ssd_count": 0, "hdd_count": 0},
            has_gpu=False, filters=self._filters(ecc_only=True),
        ) is False

    def test_price_cap(self, sample_server):
        assert server_matches(
            server=sample_server, cpu="X", threads=32, price_amount=65.0, ram_gb=128,
            disks=disk_breakdown(sample_server), has_gpu=False,
            filters=self._filters(price_cap=50.0),
        ) is False

    def test_datacenter_filter(self, sample_server):
        assert server_matches(
            server=sample_server, cpu="X", threads=32, price_amount=65.0, ram_gb=128,
            disks=disk_breakdown(sample_server), has_gpu=False,
            filters=self._filters(datacenter="HEL1"),
        ) is False
        assert server_matches(
            server=sample_server, cpu="X", threads=32, price_amount=65.0, ram_gb=128,
            disks=disk_breakdown(sample_server), has_gpu=False,
            filters=self._filters(datacenter="FSN1"),
        ) is True

    def test_ssd_drive_minimum(self, sample_server):
        disks = disk_breakdown(sample_server)
        assert server_matches(
            server=sample_server, cpu="X", threads=32, price_amount=65.0, ram_gb=128,
            disks=disks, has_gpu=False,
            filters=self._filters(disk=DiskRequirements(min_ssd_drives=3)),
        ) is False
        assert server_matches(
            server=sample_server, cpu="X", threads=32, price_amount=65.0, ram_gb=128,
            disks=disks, has_gpu=False,
            filters=self._filters(disk=DiskRequirements(min_ssd_drives=2)),
        ) is True

    def test_gpu_filter(self, sample_server):
        assert server_matches(
            server=sample_server, cpu="X", threads=32, price_amount=65.0, ram_gb=128,
            disks=disk_breakdown(sample_server), has_gpu=False,
            filters=self._filters(only_gpu=True),
        ) is False


class TestBuildRows:
    def _config(self, **kw) -> AppConfig:
        return AppConfig(
            filters=FilterConfig(ecc_only=False, disk=DiskRequirements()),
            **kw,
        )

    def test_known_cpus_scored(self, sample_servers):
        from sb_scout.models import BUILTIN_CPU_SPECS
        cfg = self._config()
        rows, unknown = build_rows(sample_servers, cfg, BUILTIN_CPU_SPECS)
        assert len(rows) == 3
        assert len(unknown) == 0

    def test_unknown_cpu_tracked(self, sample_servers):
        rows, unknown = build_rows(sample_servers, self._config(), {})
        assert len(rows) == 0
        assert len(unknown) == 3

    def test_partial_cpu_map(self, sample_servers):
        specs = {"AMD Ryzen 9 5950X": {"cores": 16, "threads": 32}}
        rows, unknown = build_rows(sample_servers, self._config(), specs)
        assert len(rows) == 1
        assert rows[0]["id"] == 9999


class TestApplyScores:
    def test_scores_populated(self, sample_servers):
        from sb_scout.models import BUILTIN_CPU_SPECS
        cfg = AppConfig(filters=FilterConfig(ecc_only=False, disk=DiskRequirements()))
        rows, _ = build_rows(sample_servers, cfg, BUILTIN_CPU_SPECS)
        apply_scores(rows, ScoreWeights())
        for r in rows:
            assert "score_cpu_rank" in r
            assert "score_storage_rank" in r
            assert "score_overall_rank" in r
            assert r["score_overall_rank"] > 0

    def test_empty_rows(self):
        apply_scores([], ScoreWeights())  # Should not raise


class TestSortRows:
    def test_descending_by_score(self):
        rows = [
            {"score_overall_rank": 0.5, "price_amount": 50},
            {"score_overall_rank": 0.9, "price_amount": 80},
            {"score_overall_rank": 0.7, "price_amount": 60},
        ]
        result = sort_rows(rows, "score_overall_rank")
        scores = [r["score_overall_rank"] for r in result]
        assert scores == [0.9, 0.7, 0.5]

    def test_tie_broken_by_price(self):
        rows = [
            {"score_overall_rank": 0.8, "price_amount": 100},
            {"score_overall_rank": 0.8, "price_amount": 50},
        ]
        result = sort_rows(rows, "score_overall_rank")
        assert result[0]["price_amount"] == 50  # cheaper first when scores tie
