import json
import os
import shutil
import subprocess
import sys
import tempfile

import numpy as np
import pytest

from feedloop import POLICIES, TOPICS, make_world, rating_items, run
from feedloop.sim import gini

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SMALL = dict(n_items=160, n_users=160, k=10)


@pytest.fixture(scope="module")
def world():
    return make_world(1, **SMALL)


@pytest.fixture(scope="module")
def runs(world):
    return {p: run(world, p, rounds=25) for p in POLICIES}


def test_world_is_sane(world):
    assert len(set(world.titles)) == world.n_items
    assert np.allclose(world.taste.max(axis=1), 1.0)
    assert world.affinity.min() >= 0 and world.affinity.max() <= 1
    items = rating_items(world)
    assert len(items) == 12 and len(set(items)) == 12
    assert sorted(set(world.topic[items])) == list(range(len(TOPICS)))


def test_gini():
    assert gini([1, 1, 1, 1]) == 0
    assert abs(gini([0, 0, 0, 4]) - 0.75) < 1e-12


def test_pure_exploit_narrows(runs):
    m = runs["exploit"]["metrics"]
    assert m["diversity"][0] > 5.5                  # round 1 is a random feed
    assert m["diversity"][-1] < m["diversity"][0] - 1
    assert m["coverage"][0] > 0.95 and m["coverage"][-1] < 0.7
    assert m["gini"][-1] > m["gini"][0] + 0.3
    assert max(m["bubble"]) > 2 * m["bubble"][0]       # feeds lean into each user's top topic
    assert m["utility"][-1] > m["utility"][0]        # it does learn what people like


def test_exploration_opens_the_catalogue(runs):
    ex = runs["exploit"]["metrics"]
    for p in ("eps", "ucb", "thompson"):
        m = runs[p]["metrics"]
        assert np.mean(m["coverage"][-5:]) > np.mean(ex["coverage"][-5:]) + 0.1, p
    for p in ("eps", "thompson"):
        assert np.mean(runs[p]["metrics"]["gini"][-5:]) < np.mean(ex["gini"][-5:]) - 0.05, p
    assert runs["random"]["metrics"]["coverage"][-1] > 0.95


def test_mmr_keeps_feeds_mixed(runs):
    assert runs["mmr"]["metrics"]["diversity"][-1] > runs["exploit"]["metrics"]["diversity"][-1] + 1.5


def test_seeded_determinism(world):
    a = run(world, "thompson", rounds=6, watch=0)
    b = run(world, "thompson", rounds=6, watch=0)
    assert a["metrics"] == b["metrics"] and a["watched"] == b["watched"]
    c = run(make_world(2, **SMALL), "thompson", rounds=6, watch=0)
    assert c["watched"]["feeds"] != a["watched"]["feeds"]


def test_switch_shares_the_past(world):
    plain = run(world, "exploit", rounds=16)
    bent = run(world, "exploit", rounds=16, switch_round=9, switch_policy="ucb")
    for m in ("diversity", "coverage", "gini"):
        assert plain["metrics"][m][:8] == bent["metrics"][m][:8]
    assert bent["metrics"]["coverage"][-1] > plain["metrics"]["coverage"][-1] + 0.1


def test_new_item_cold_start():
    w = make_world(1, new_item=True, **SMALL)
    ex = run(w, "exploit", rounds=14, new_item_round=5)
    ucb = run(w, "ucb", rounds=14, new_item_round=5)
    assert ex["new_item_first_round"] is None
    assert ucb["new_item_first_round"] == 5
    assert sum(ex["metrics"]["new_item"][:4]) == 0 and sum(ucb["metrics"]["new_item"][:4]) == 0


def test_visitor_is_a_user():
    ratings = {0: True, 1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False}
    w = make_world(1, visitor_ratings=ratings, **SMALL)
    assert w.n_users == SMALL["n_users"] + 1
    r = run(w, "exploit", rounds=5, visitor_ratings=ratings)
    assert r["watched"]["user"] == w.n_users - 1
    assert len(r["watched"]["feeds"]) == 5 and len(r["watched"]["feeds"][0]) == 10
    liked = [t for t in range(8) if ratings[t]]
    assert np.allclose(w.taste[-1][liked], 1.0)


@pytest.mark.skipif(shutil.which("node") is None, reason="needs node")
@pytest.mark.parametrize("policy", ["exploit", "eps", "ucb", "thompson", "mmr"])
def test_js_parity(policy):
    """web/feedloop.js must give the same feeds, clicks and metrics as the Python."""
    ratings = {"0": True, "5": False, "10": True, "17": False}
    cfg = dict(seed=3, nItems=120, nUsers=80, k=10, policy=policy, rounds=8, newItemRound=3, visitorRatings=ratings)
    with tempfile.TemporaryDirectory() as d:
        p = os.path.join(d, "cfg.json")
        with open(p, "w") as f:
            json.dump(cfg, f)
        js = json.loads(subprocess.run(["node", os.path.join(ROOT, "tests", "run_js.js"), p], check=True, capture_output=True).stdout)
    vr = {int(k): v for k, v in ratings.items()}
    w = make_world(3, 120, 80, 10, new_item=True, visitor_ratings=vr)
    py = run(w, policy, 8, new_item_round=3, visitor_ratings=vr)
    assert w.titles[:20] == js["titles"]
    assert rating_items(w) == js["ratingItems"]
    assert py["watched"]["feeds"] == js["watched"]["feeds"]
    assert py["watched"]["clicks"] == js["watched"]["clicks"]
    assert py["item_imps"] == js["item_imps"]
    for m, ys in py["metrics"].items():
        assert np.allclose(ys, js["metrics"][m], atol=1e-9), m
