# Foundation tests run before extending architectural content.
from dataclasses import dataclass
import pytest
from hearth import Scene, Box, Frame, Binding, ContractError
from hearth.kernel import Capability, Contract, Plan, Child, Rule
from hearth.components.primitives import Assembly, Volume, Wall, Window, Grass, Tree, Pool
from hearth.environment import Terrain, constant
from hearth.blocks import state_of, blockstates_equivalent
from hearth.persistence import export_scene, load_scene, semantic_digest


def land():
    s = Scene(12, domain=Box((-10, -3, -10), (20, 25, 20)))
    s.place('terrain', Terrain(s.domain, constant(0)))
    return s


def test_composite_attachment_automatic_chain():
    s = Scene(4)
    children = (Child('base', Volume(Box((0, 0, 0), (8, 0, 0)), state_of('stone'))), Child('wall', Wall(), frame=Frame((0, 1, 0))), Child('window', Window(), frame=Frame((2, 2, 0)), bindings=(Binding('/house/wall', 'infill'),)))
    s.place('house', Assembly('house', Box((-1, 0, -2), (10, 8, 3)), children))
    q = s.inspect((2, 2, 0))
    assert [n['type'] for n in q['chain']] == ['window.shuttered', 'wall.framed', 'house']
    assert len(s.cells('/house/window')) == 10
    assert s.cells('/house/window') <= s.cells('/house', True)


def test_substitution_same_parent():

    def parent(window):
        return Assembly('room', Box((-1, -1, -2), (10, 8, 3)), (Child('base', Volume(Box((0, -1, 0), (8, -1, 0)), state_of('stone'))), Child('wall', Wall()), Child('window', window, frame=Frame((2, 1, 0)), bindings=(Binding('/room/wall', 'infill'),))))

    a, b = Scene(), Scene()
    a.place('room', parent(Window()))
    b.place('room', parent(Window(shutters=False)))
    assert semantic_digest(a) != semantic_digest(b)
    assert a.nodes['/room/window'].contract.envelope == b.nodes['/room/window'].contract.envelope


def test_tree_grass_and_pool_preserve_support():
    s = land()
    s.place('grass', Grass(), frame=Frame((0, 1, 0)), bindings=(Binding('/terrain', 'planting', 'plant'),))
    s.place('tree', Tree(), frame=Frame((0, 1, 0)), bindings=(Binding('/terrain', 'planting', 'plant'), Binding('/grass', 'replacement', 'plant')))
    assert '/grass' not in s.nodes
    assert s.inspect((0, 1, 0))['instance'] == '/tree'
    before = semantic_digest(s)
    nodes = set(s.nodes)
    ops = set(s.operations)
    with pytest.raises(ContractError):
        s.place('pool', Pool(), frame=Frame((-2, 0, -2)), bindings=(Binding('/terrain', 'construction', 'excavate'),))
    assert semantic_digest(s) == before and set(s.nodes) == nodes and set(s.operations) == ops
    with pytest.raises(ContractError):
        s.place('grass2', Grass(), frame=Frame((0, 1, 0)))
    s.place('pool-ok', Pool(), frame=Frame((6, 0, 6)), bindings=(Binding('/terrain', 'construction', 'excavate'),))
    s.finalize()


def test_tall_grass_whole_object_and_no_tree_excavation():
    s = land()
    s.place('grass', Grass(True), frame=Frame((0, 1, 0)))
    with pytest.raises(ContractError):
        s.place('ordinary-tree', Tree(), frame=Frame((0, 0, 0)), bindings=(Binding('/terrain', 'construction', 'excavate'),))
    s.place('tree', Tree(), frame=Frame((0, 1, 0)), bindings=(Binding('/grass', 'replacement', 'plant'),))
    assert '/grass' not in s.nodes


def test_transforms_and_persistence(work):
    frame = Frame((14, 3, -7), 1)
    s = Scene(-42)
    s.place('beam', Volume(Box((0, 0, 0), (2, 0, 0)), state_of('oak_log', axis='x')), frame=frame)
    assert s.inspect((14, 3, -5))['state'] == state_of('oak_log', axis='z')
    path = work / 'space name.litematic'
    export_scene(s, path)
    restored = load_scene(path)
    assert restored.inspect((14, 3, -5)) == s.inspect((14, 3, -5))
    assert blockstates_equivalent('minecraft:oak_stairs[facing=north,half=bottom]', 'minecraft:oak_stairs[half=bottom,facing=north]')
    assert not blockstates_equivalent('minecraft:oak_log', 'minecraft:oak_log[axis=y]')
    import json
    meta = path.with_suffix('.litematic.structure.json')
    data = json.loads(meta.read_text())
    data['digest'] = 'bad'
    meta.write_text(json.dumps(data))
    with pytest.raises(ContractError, match='geometry-mismatch'):
        load_scene(path)


def test_clearance_rollback_and_bounds():
    s = Scene()
    s.place('stairs', Assembly('circulation', Box((0, 0, 0), (3, 5, 3)), (), (Rule('clear', ((1, 2, 1), (1, 3, 1))),)))
    with pytest.raises(ContractError, match='clearance'):
        s.place('decoration', Volume(Box((1, 3, 1), (1, 3, 1)), state_of('stone')))
    assert set(s.nodes) == {'/stairs'} and not s.blocks

    @dataclass
    class Escaper:

        def capability(self):
            return Capability('bad', ())

        def negotiate(self, c, p):
            return Contract(Box((0, 0, 0), (0, 0, 0)))

        def realize(self, c, b):
            p = Plan()
            p.block((1, 0, 0), state_of('stone'))
            return p

    with pytest.raises(ContractError, match='envelope'):
        s.place('bad', Escaper())


def test_seed_stability_and_regeneration():
    from hearth.randomness import Scope
    a = Scope(123).child('house')
    before = a.stream('layout').getrandbits(64)
    Scope(123).child('failed').stream().random()
    assert before == a.stream('layout').getrandbits(64)
    s = Scene()
    s.place('beam', Volume(Box((0, 0, 0), (2, 0, 0)), state_of('stone')))
    s.regenerate('/beam', Volume(Box((0, 0, 0), (1, 0, 0)), state_of('oak_planks')))
    assert s.inspect((2, 0, 0))['instance'] is None
    assert len(s.cells('/beam')) == 2
