from itertools import chain

import pytest

from comb_spec_searcher import CartesianProduct
from tilings import GriddedPerm, Tiling
from tilings.strategies import FactorFactory
from tilings.strategies.factor import FactorStrategy, Interleaving

pytest_plugins = [
    "tests.fixtures.simple_tiling",
    "tests.fixtures.diverse_tiling",
    "tests.fixtures.no_point_tiling",
]


@pytest.fixture
def tiling():
    t = Tiling(
        obstructions=(
            GriddedPerm((2, 1, 0), ((0, 0),) * 3),
            GriddedPerm((0, 1, 2), ((1, 2),) * 3),
            GriddedPerm((2, 0, 1), ((3, 0),) * 3),
            GriddedPerm((1, 0), ((1, 1),) * 2),
            GriddedPerm((1, 0), ((2, 2),) * 2),
            GriddedPerm((0, 1), ((1, 1), (2, 2))),
        )
    )
    return t


@pytest.fixture
def not_fact_tiling():
    not_fact_tiling = Tiling(
        obstructions=[
            GriddedPerm((0, 1), ((0, 0), (0, 0))),
            GriddedPerm((0, 1), ((0, 0), (0, 1))),
            GriddedPerm((0, 1), ((0, 1), (0, 1))),
        ]
    )
    return not_fact_tiling


@pytest.fixture
def factor_rules(tiling):
    return [s(tiling) for s in FactorFactory()(tiling)]


@pytest.fixture
def factor_with_int_rules(tiling):
    return [s(tiling) for s in FactorFactory(interleaving="all")(tiling)]


@pytest.fixture
def factor_with_mon_int_rules(tiling):
    return [s(tiling) for s in FactorFactory(interleaving="monotone")(tiling)]


def test_not_factorable(simple_tiling):
    assert len(list(FactorFactory()(simple_tiling))) == 0
    assert len(list(FactorFactory(interleaving="monotone")(simple_tiling))) == 0
    assert len(list(FactorFactory(interleaving="all")(simple_tiling))) == 0


def test_with_unions(diverse_tiling):
    strats = list(FactorFactory(interleaving="all", unions=True)(diverse_tiling))
    assert len(strats) == 14
    assert sum(1 for s in strats if s.workable) == 1
    assert sum(1 for s in strats if not s.workable) == 13


def test_with_unions_not_workable(diverse_tiling):
    strats = list(
        FactorFactory(interleaving="all", unions=True, workable=False)(diverse_tiling)
    )
    assert sum(1 for s in strats if s.workable) == 0
    assert sum(1 for s in strats if not s.workable) == 14


def test_standard_factor():
    tiling = Tiling(
        obstructions=[GriddedPerm((0, 1), [(0, 0), (0, 0)])],
        requirements=[
            [
                GriddedPerm((0, 1), [(1, 1), (1, 1)]),
                GriddedPerm((0, 1), [(1, 1), (1, 2)]),
            ]
        ],
    )
    strats = [s(tiling).children for s in FactorFactory()(tiling)]
    assert len(strats) == 1
    factors = strats[0]
    assert set(factors) == set(
        [
            Tiling(
                requirements=[
                    [
                        GriddedPerm((0, 1), [(0, 0), (0, 0)]),
                        GriddedPerm((0, 1), [(0, 0), (0, 1)]),
                    ]
                ]
            ),
            Tiling(obstructions=[GriddedPerm((0, 1), [(0, 0), (0, 0)])]),
        ]
    )


def test_factor_all_interleaving(diverse_tiling):
    strats = [
        s(diverse_tiling).children
        for s in FactorFactory(interleaving="all")(diverse_tiling)
    ]
    assert len(strats) == 1
    factors = strats[0]
    assert len(factors) == 4
    print(diverse_tiling)
    assert set(factors) == set(
        [
            Tiling(
                obstructions=[
                    GriddedPerm((0, 2, 3, 1), [(0, 0), (1, 1), (1, 1), (2, 0)])
                ],
                requirements=[[GriddedPerm((0,), [(2, 0)])]],
            ),
            Tiling(
                obstructions=[
                    GriddedPerm((0, 1), [(0, 0), (0, 0)]),
                    GriddedPerm((1, 0), [(0, 0), (0, 0)]),
                ],
                requirements=[[GriddedPerm((0,), [(0, 0)])]],
            ),
            Tiling(
                obstructions=[
                    GriddedPerm((0, 1), [(0, 0), (0, 0)]),
                    GriddedPerm((1, 0), [(0, 0), (0, 0)]),
                ],
                requirements=[[GriddedPerm((0,), [(0, 0)])]],
            ),
            Tiling(
                requirements=[
                    [
                        GriddedPerm((1, 0), [(0, 2), (0, 1)]),
                        GriddedPerm((0, 2, 1), [(0, 1), (0, 2), (1, 2)]),
                    ]
                ]
            ),
        ]
    )


def test_str():
    assert str(FactorFactory()) == "factor"
    assert (
        str(FactorFactory(interleaving="monotone"))
        == "factor with monotone interleaving"
    )
    assert str(FactorFactory(interleaving="all")) == "factor with interleaving"


# ------------------------------------------------------------
#       Test for all classes
# ------------------------------------------------------------


def test_formal_step(factor_rules, factor_with_int_rules, factor_with_mon_int_rules):
    all_rules = factor_rules + factor_with_int_rules + factor_with_mon_int_rules
    for rule in all_rules:
        assert (
            sum(1 for c in rule.formal_step if c == "/")
            == len(rule.strategy.partition) - 1
        )
        assert sum(1 for c in rule.formal_step if c == "{") == len(
            rule.strategy.partition
        )
        assert sum(1 for c in rule.formal_step if c == "}") == len(
            rule.strategy.partition
        )
        assert all(
            all(str(cell) in rule.formal_step for cell in p)
            for p in rule.strategy.partition
        )
        assert all("factor with partition " in rule.formal_step for rule in all_rules)
        assert " + " not in str(rule)
        assert " x " in str(rule) or " * " in str(rule)


def test_constructor(factor_rules, factor_with_int_rules, factor_with_mon_int_rules):
    assert all(rule.constructor.__class__ == CartesianProduct for rule in factor_rules)
    for rule in chain(factor_with_int_rules, factor_with_mon_int_rules):
        with pytest.raises(NotImplementedError):
            assert isinstance(rule.constructor, Interleaving)


def test_rule(
    factor_rules, factor_with_int_rules, factor_with_mon_int_rules, not_fact_tiling
):
    all_rules = factor_rules + factor_with_int_rules + factor_with_mon_int_rules
    assert all(not rule.inferrable for rule in factor_rules)
    assert all(rule.inferrable for rule in factor_with_mon_int_rules)
    assert all(rule.inferrable for rule in factor_with_int_rules)
    assert all(len(rule.strategy.partition) == len(rule.children) for rule in all_rules)
    assert all(rule.workable for rule in all_rules)
    assert all(not rule.possibly_empty for rule in all_rules)
    assert all(rule.ignore_parent for rule in all_rules)
    assert not list(FactorFactory(interleaving="all")(not_fact_tiling))


def test_all_union_rules():
    t = Tiling(
        obstructions=[
            GriddedPerm((0, 1), ((0, 0),) * 2),
            GriddedPerm((0, 1), ((1, 1),) * 2),
            GriddedPerm((0, 1), ((2, 2),) * 2),
        ]
    )
    fo = FactorFactory(unions=True)(t)
    f2 = Tiling(obstructions=[GriddedPerm((0, 1), ((0, 0),) * 2)])
    # The full factorisation rule is not returned
    assert all(rule(t).children != [f2, f2, f2] for rule in fo)
    # The tiling are marked as not workable by default
    assert all(not rule(t).workable for rule in fo)
    # The workable can be turned on for union of factor
    assert all(rule(t).workable for rule in fo)


def test_interleaving_return_normal_factor():
    """
    In the case where there is no interleaving we make sure that an interleaving factory
    still return a plain cartesian product like factor strategy.
    """
    t = Tiling(
        obstructions=[
            GriddedPerm.single_cell((0, 1), (0, 0)),
            GriddedPerm.single_cell((0, 1), (1, 1)),
        ]
    )
    factor_strat = next(FactorFactory(interleaving="all")(t))
    assert factor_strat.formal_step() == "factor with partition {(0, 0)} / {(1, 1)}"
    assert factor_strat.__class__ == FactorStrategy
    rule = factor_strat(t)
    assert rule.constructor.__class__ == CartesianProduct
