"""Tests for py-concrete-calc."""

import math
import pytest

from concrete_calc import (
    slab,
    cylinder,
    footing,
    stairs,
    curb,
    hollow_cylinder,
    circular_slab,
    triangular_prism,
    grade_beam,
    wall,
    cubic_feet_to_yards,
    cubic_yards_to_cubic_feet,
    cubic_feet_to_meters,
    cubic_meters_to_yards,
    inches_to_feet,
    cm_to_meters,
    bag_estimate,
    bags_from_yards,
    ready_mix_cost,
    bag_cost,
    rebar_estimate,
    wire_mesh_estimate,
    form_lumber_estimate,
    get_regional_price,
    list_regions,
    estimate,
    ConcreteCalculator,
    to_dict,
    to_json,
    to_csv,
)


# ── Shapes ──────────────────────────────────────────────────────────────────


class TestSlab:
    def test_basic(self):
        assert slab(10, 10, 0.5) == 50.0

    def test_zero(self):
        assert slab(10, 10, 0) == 0.0


class TestCylinder:
    def test_basic(self):
        vol = cylinder(2, 4)
        assert vol == pytest.approx(math.pi * 1 * 4, rel=1e-9)


class TestFooting:
    def test_basic(self):
        assert footing(20, 1.5, 1) == 30.0


class TestStairs:
    def test_three_steps(self):
        vol = stairs(0.5, 1.0, 3.0, 3)
        # step1: 0.5*1*1*3=1.5, step2: 0.5*2*1*3=3, step3: 0.5*3*1*3=4.5
        assert vol == pytest.approx(9.0)


class TestCurb:
    def test_basic(self):
        assert curb(50, 0.5, 1.5) == 37.5


class TestHollowCylinder:
    def test_basic(self):
        vol = hollow_cylinder(4, 2, 10)
        expected = math.pi * (4 - 1) * 10
        assert vol == pytest.approx(expected, rel=1e-9)


class TestCircularSlab:
    def test_basic(self):
        vol = circular_slab(10, 0.5)
        assert vol == pytest.approx(math.pi * 25 * 0.5, rel=1e-9)


class TestTriangularPrism:
    def test_basic(self):
        assert triangular_prism(4, 3, 10) == pytest.approx(60.0)


class TestGradeBeam:
    def test_single(self):
        assert grade_beam(20, 1, 2) == 40.0

    def test_multiple(self):
        assert grade_beam(20, 1, 2, count=3) == 120.0


class TestWall:
    def test_basic(self):
        assert wall(20, 8, 0.5) == 80.0


# ── Units ───────────────────────────────────────────────────────────────────


class TestUnits:
    def test_feet_to_yards(self):
        assert cubic_feet_to_yards(27) == pytest.approx(1.0)

    def test_yards_to_feet(self):
        assert cubic_yards_to_cubic_feet(1) == 27

    def test_feet_to_meters(self):
        assert cubic_feet_to_meters(35.3147) == pytest.approx(1.0, rel=1e-4)

    def test_meters_to_yards(self):
        assert cubic_meters_to_yards(0.764555) == pytest.approx(1.0, rel=1e-4)

    def test_inches_to_feet(self):
        assert inches_to_feet(6) == pytest.approx(0.5)

    def test_cm_to_meters(self):
        assert cm_to_meters(150) == pytest.approx(1.5)


# ── Bags ────────────────────────────────────────────────────────────────────


class TestBags:
    def test_80lb(self):
        assert bag_estimate(6.0, 80) == 10

    def test_60lb(self):
        assert bag_estimate(4.5, 60) == 10

    def test_bad_size(self):
        with pytest.raises(ValueError):
            bag_estimate(10, 99)

    def test_from_yards(self):
        assert bags_from_yards(1, 80) == 45


# ── Cost ────────────────────────────────────────────────────────────────────


class TestCost:
    def test_ready_mix(self):
        r = ready_mix_cost(5, 150)
        assert r["cost"] == 750.0
        assert r["short_load"] is False

    def test_short_load(self):
        r = ready_mix_cost(0.5, 150, 50, 1)
        assert r["short_load"] is True
        assert r["cost"] == 125.0

    def test_bag_cost(self):
        r = bag_cost(6.0, 80, 6.5)
        assert r["bags"] == 10
        assert r["cost"] == 65.0


# ── Materials ───────────────────────────────────────────────────────────────


class TestMaterials:
    def test_rebar(self):
        r = rebar_estimate(20, 10, spacing_in=12, bar_size=4)
        assert r["pieces_20ft"] > 0
        assert r["weight_lbs"] > 0

    def test_rebar_bad_size(self):
        with pytest.raises(ValueError):
            rebar_estimate(10, 10, bar_size=99)

    def test_wire_mesh(self):
        r = wire_mesh_estimate(20, 10)
        assert r["sheets_5x10"] > 0
        assert r["area_sqft"] == 200.0

    def test_forms(self):
        r = form_lumber_estimate(20, 10)
        assert r["perimeter_ft"] == 60.0
        assert r["boards_8ft"] == 8
        assert r["stakes"] > 0


# ── Regional costs ──────────────────────────────────────────────────────────


class TestRegional:
    def test_national(self):
        r = get_regional_price("national")
        assert r["price_per_yard"] == 150.0

    def test_northeast(self):
        r = get_regional_price("northeast")
        assert r["price_per_yard"] == 175.0

    def test_bad_region(self):
        with pytest.raises(ValueError):
            get_regional_price("mars")

    def test_list_regions(self):
        regions = list_regions()
        assert "national" in regions
        assert len(regions) == 6


# ── Estimate ────────────────────────────────────────────────────────────────


class TestEstimate:
    def test_basic(self):
        r = estimate(20, 10, 0.5)
        assert r.cubic_feet > 0
        assert r.cubic_yards > 0
        assert r.bags > 0

    def test_with_materials(self):
        r = estimate(20, 10, 0.5, include_materials=True)
        assert r.rebar is not None
        assert r.wire_mesh is not None
        assert r.forms is not None

    def test_waste_factor(self):
        r1 = estimate(10, 10, 0.5, waste_factor=1.0)
        r2 = estimate(10, 10, 0.5, waste_factor=1.2)
        assert r2.cubic_feet > r1.cubic_feet


# ── Export ──────────────────────────────────────────────────────────────────


class TestExport:
    def test_to_dict(self):
        r = estimate(10, 10, 0.5)
        d = to_dict(r)
        assert isinstance(d, dict)
        assert "cubic_feet" in d

    def test_to_json(self):
        r = estimate(10, 10, 0.5)
        s = to_json(r)
        assert '"cubic_feet"' in s

    def test_to_csv(self):
        rows = [estimate(10, 10, 0.5), estimate(20, 10, 0.5)]
        s = to_csv(rows)
        assert "cubic_feet" in s
        lines = s.strip().split("\n")
        assert len(lines) == 3  # header + 2 data rows


# ── ConcreteCalculator class ────────────────────────────────────────────────


class TestCalculatorClass:
    def test_slab(self):
        calc = ConcreteCalculator()
        assert calc.slab(10, 10, 0.5) == 50.0

    def test_estimate(self):
        calc = ConcreteCalculator()
        r = calc.estimate(20, 10, 0.5)
        assert r.bags > 0
