import pytest

from delivery.csp import AssignmentCSP, check_assignment
from delivery.instances import Instance, Order, Van, generate_instance
from delivery.integration import explain_no_solution
from conftest import line_tm

SOLVERS = [(False, False), (True, False), (True, True)]


def three_vans(cap=10, stops=10):
    return [Van(0, cap, 480, 1020, stops), Van(1, cap, 540, 1080, stops), Van(2, cap, 600, 1140, stops)]


# ----------------------------------------------------------------------------- normal cases
@pytest.mark.parametrize("seed", range(3))
def test_all_solvers_valid_and_identical(tm, seed):
    inst = generate_instance(18, seed, n_zones=tm.Z, cap_slack=1.15)
    csp = AssignmentCSP(inst, tm)
    sols = []
    for fc, ac3 in SOLVERS:
        r = csp.solve(fc=fc, ac3=ac3)
        assert r.status == "solved"
        assert check_assignment(csp, r.assignment) == []
        sols.append(r.assignment)
    assert sols[0] == sols[1] == sols[2]          # same orderings -> same first solution


def test_forward_checking_never_needs_more_assignments(tm):
    for seed in range(5):
        inst = generate_instance(24, seed, n_zones=tm.Z, cap_slack=1.05, max_stops=8)
        csp = AssignmentCSP(inst, tm)
        bt = csp.solve(fc=False)
        fc = csp.solve(fc=True)
        mac = csp.solve(fc=True, ac3=True)
        assert fc.stats["assignments"] <= bt.stats["assignments"]
        assert mac.stats["assignments"] <= fc.stats["assignments"]


def test_ac3_prunes_triangle_with_a_fixed_order():
    # three mutually conflicting orders, order 0 can only use van 0 -> AC-3 forces the rest
    tm = line_tm()
    orders = [Order(0, 4, 480, 900, 1, 5), Order(1, 4, 480, 900, 1, 5), Order(2, 4, 480, 900, 1, 5)]
    inst = Instance(orders, three_vans(), 0, 1)
    csp = AssignmentCSP(inst, tm)
    csp.domains0 = {0: [0], 1: [0, 1], 2: [0, 1, 2]}
    for a, b in ((0, 1), (0, 2), (1, 2)):
        csp.add_edge(a, b, "nn")
    r = csp.solve(fc=True, ac3=True)
    assert r.assignment == {0: 0, 1: 1, 2: 2}
    assert r.stats["ac3_prunes"] >= 2 and r.stats["backtracks"] == 0


def test_pairwise_conflict_edge_is_created_from_predicted_times():
    tm = line_tm()          # 0 -> 4 takes 21 min, 4 -> 0 takes 21 min
    a = Order(0, 4, 480, 505, 1, 5)        # must be at the far end by 08:25
    b = Order(1, 0, 495, 505, 1, 5)        # and at the depot zone 08:15-08:25
    c = Order(2, 1, 480, 900, 1, 5)        # unconstrained
    csp = AssignmentCSP(Instance([a, b, c], three_vans(), 0, 1), tm)
    assert frozenset((0, 1)) in csp.edges
    assert frozenset((0, 2)) not in csp.edges


# ----------------------------------------------------------------------------- edge case
def test_edge_late_shift_van_removed_from_domain_by_unary_constraint():
    tm = line_tm()
    o = Order(0, 2, 480, 560, 1, 5)        # closes 09:20; van 2 starts 10:00
    csp = AssignmentCSP(Instance([o], three_vans(), 0, 1), tm)
    assert csp.domains0[0] == [0, 1]
    assert "van 2" in csp.unary_reasons[0][0]


# ----------------------------------------------------------------------------- impossible
def test_impossible_four_mutually_conflicting_orders_three_vans():
    tm = line_tm()
    orders = [Order(i, 4 if i % 2 else 0, 480, 900, 1, 5) for i in range(5)]
    inst = Instance(orders, three_vans(), 0, 1)
    csp = AssignmentCSP(inst, tm)
    for a in range(4):
        for b in range(a + 1, 4):
            csp.add_edge(a, b, "learned")
    for fc, ac3 in SOLVERS:
        assert csp.solve(fc=fc, ac3=ac3).status == "unsat"
    reasons = explain_no_solution(csp, inst, tm)
    assert any("pairwise incompatible" in r for _, r in reasons)
    assert {oid for oid, _ in reasons} == {0, 1, 2, 3}


def test_impossible_capacity():
    tm = line_tm()
    orders = [Order(i, 1, 480, 900, 3, 5) for i in range(4)]       # 12 units
    inst = Instance(orders, three_vans(cap=3), 0, 1)                 # 9 units
    csp = AssignmentCSP(inst, tm)
    assert csp.solve(fc=True, ac3=True).status == "unsat"
    reasons = explain_no_solution(csp, inst, tm)
    print("\n".join(r for _, r in reasons))
    assert any("exceeds total van capacity" in r for _, r in reasons)
