# Exercise public composition, dependent domains and explicit intent preservation.
from dataclasses import replace

import pytest

from hearthwright import Choice, Span, Design, DesignError, Level, Volume, Roof, Join, Site, Feature, Materials, Limits, generate, resolve, SiteComponent, register_site_component


def test_composition_needs_no_reference_dispatch():
    design = Design('Independent client', (
        Volume('home', width=Span(13, 17, 2), depth=15, levels=(Level(('living', 'kitchen')), Level(('sleeping',), attic=True)), roof=Roof(ridge=Choice('x', 'z'), pitch=1.0)),
        Volume('work', width=9, depth=11, join=Join('home', 'east', Choice('front', 'back')), levels=(Level(('workshop', 'storage')),), roof=Roof(form='hip', pitch=.5)),
    ), materials=Materials(wall='stone', roof='red'))
    building = generate(design, 42)
    assert building.validate().valid
    assert building.plan.masses[1].join['to'] == 'home'
    assert building.plan.masses[0].rect.width in (13, 15, 17)
    assert building.plan.masses[1].roof['form'] == 'hip'


def test_bounded_floor_count_changes_actual_geometry():
    from examples.orchard_house import DESIGN
    buildings = [generate(DESIGN, seed) for seed in range(5)]
    assert {len(b.plan.masses[0].levels) for b in buildings} == {1, 2}
    assert len({b.metadata['geometry_signature'] for b in buildings}) > 1


def test_conditional_choice_does_not_discard_explicit_roof():
    design = Design('Conditional loft', (Volume('home', width=15, depth=13, levels=(Level(('living', 'kitchen')), Level(('sleeping',), attic=True)), roof=Roof(pitch=Choice(.5, 1.0, 1.5), dormers=1)),))
    for seed in range(6):
        plan = resolve(design, seed)
        assert plan.masses[0].roof['pitch'] in (1.0, 1.5)
        assert plan.masses[0].roof['dormers'] == 1


def test_planner_backtracks_occupied_wing_domains():
    design = Design('Two wings', (
        Volume('main', width=13, depth=13),
        Volume('first', join=Join('main', 'east'), width=9, depth=9),
        Volume('second', join=Join('main', Choice('east', 'west')), width=9, depth=9),
    ))
    for seed in range(4):
        building = generate(design, seed)
        assert building.plan.masses[2].join['side'] == 'west'


@pytest.mark.parametrize('design,match', [
    (Design('bad water', (Volume('house'),), site=Site(terrain='water')), 'water buildings'),
    (Design('low loft', (Volume('house', levels=(Level(('sleeping',), attic=True),), roof=Roof(pitch=.5)),)), 'roof.pitch'),
    (Design('root roof', (Volume('root', envelope='roots'),)), 'roof=None'),
    (Design('tiny', (Volume('home', width=7),)), 'width'),
    (Design('duplicate', (Volume('home'), Volume('home'))), 'duplicate'),
    (Design('oversized', (Volume('home'),), limits=Limits(horizontal=15)), 'resource limit'),
    (Design('unsupported material', (Volume('home'),), materials=Materials(roof='wool')), 'roof role'),
    (Design('missing join', (Volume('a'), Volume('b'))), 'Join'),
    (Design('undefined mound', (Volume('home'),), site=Site(terrain='hill')), 'hill terrain needs'),
])
def test_reject_contradictory_intent(design, match):
    with pytest.raises(DesignError, match=match):
        generate(design, 0)


@pytest.mark.parametrize('seed', [-(2**63) - 1, 2**63, 1.5, '1', True])
def test_seed_domain(seed):
    with pytest.raises(DesignError, match='signed 64-bit'):
        resolve(Design('seed', (Volume('home'),)), seed)


def test_invalid_domains():
    with pytest.raises(ValueError):
        Span(10, 3)
    with pytest.raises(ValueError):
        Span(3, 10, 0)
    with pytest.raises(ValueError):
        Choice()
    with pytest.raises(ValueError):
        Span(1, 1000)
    with pytest.raises(ValueError):
        Span(1.5, 8)


def test_windmill_clearance_filters_choices_without_expanding_explicit_scale():
    with pytest.raises(DesignError, match='windmill.scale'):
        resolve(Design('Small sails', (Volume('home'),), features=(Feature('windmill', scale=7),)), 0)
    plan = resolve(Design('Clear sails', (Volume('home'),), features=(Feature('windmill', scale=Choice(7, 9)),)), 0)
    assert plan.features[0]['rect'].width == 9


def test_registered_component_uses_shared_path_and_validation():
    from hearthwright.components import SITE_COMPONENTS
    calls = []

    def realize(ctx, feature):
        r = feature['rect']
        x, z = r.center
        y = ctx.ground[x, z]
        ctx.g.put(x, y + 1, z, 'barrel')
        ctx.g.lantern(x, y + 2, z, feature['id'])
        feature['target'] = (x, y + 1, r.z1 + 1)
        ctx.g.metadata['features'].append({'owner': feature['id'], 'kind': feature['kind'], 'target': feature['target']})

    def rule(grid, metadata, report):
        calls.append(len(metadata['features']))

    component = SiteComponent('tool_cache', realize, rule=rule, version='test-1')
    try:
        register_site_component(component)
        building = generate(Design('Extension', (Volume('home'),), features=(Feature('tool_cache'),)), 0)
        assert building.validate().valid
        assert calls
        assert building.plan.features[0]['version'] == 'test-1'
    finally:
        SITE_COMPONENTS.pop('tool_cache', None)
