# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2025 Furgo                                              *
# *                                                                         *
# *   This file is part of FreeCAD.                                         *
# *                                                                         *
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
# *   under the terms of the GNU Lesser General Public License as           *
# *   published by the Free Software Foundation, either version 2.1 of the  *
# *   License, or (at your option) any later version.                       *
# *                                                                         *
# *   FreeCAD is distributed in the hope that it will be useful, but        *
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
# *   Lesser General Public License for more details.                       *
# *                                                                         *
# *   You should have received a copy of the GNU Lesser General Public      *
# *   License along with FreeCAD. If not, see                               *
# *   <https://www.gnu.org/licenses/>.                                      *
# *                                                                         *
# ***************************************************************************

# Unit tests for the ArchComponent module

import Arch
import Draft
import Part
import FreeCAD as App
from bimtests import TestArchBase
from draftutils.messages import _msg

from math import pi, cos, sin, radians


class TestArchComponent(TestArchBase.TestArchBase):

    def testAdd(self):
        App.Console.PrintLog("Checking Arch Add...\n")
        l = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(2, 0, 0))
        w = Arch.makeWall(l, width=0.2, height=2)
        sb = Part.makeBox(1, 1, 1)
        b = App.ActiveDocument.addObject("Part::Feature", "Box")
        b.Shape = sb
        App.ActiveDocument.recompute()
        Arch.addComponents(b, w)
        App.ActiveDocument.recompute()
        r = w.Shape.Volume > 1.5
        self.assertTrue(r, "Arch Add failed")

    def testRemove(self):
        App.Console.PrintLog("Checking Arch Remove...\n")
        l = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(2, 0, 0))
        w = Arch.makeWall(l, width=0.2, height=2, align="Right")
        sb = Part.makeBox(1, 1, 1)
        b = App.ActiveDocument.addObject("Part::Feature", "Box")
        b.Shape = sb
        App.ActiveDocument.recompute()
        Arch.removeComponents(b, w)
        App.ActiveDocument.recompute()
        r = w.Shape.Volume < 0.75
        self.assertTrue(r, "Arch Remove failed")

    def testBsplineSlabAreas(self):
        """Test the HorizontalArea and VerticalArea properties of a Bspline-based slab.

        See https://github.com/FreeCAD/FreeCAD/issues/20989.
        """

        operation = "Checking Bspline slab area calculation..."
        self.printTestMessage(operation)

        doc = App.ActiveDocument

        # Parameters
        radius = 10000  # 10 meters in mm
        extrusionLength = 100  # 10 cm in mm
        numPoints = 50  # Number of points for B-spline
        startAngle = 0  # Start at 0 degrees (right)
        endAngle = 180  # End at 180 degrees (left)

        # Create points for semicircle
        points = []
        angleStep = (endAngle - startAngle) / (numPoints - 1)
        for i in range(numPoints):
            angleDeg = startAngle + i * angleStep
            angleRad = radians(angleDeg)
            x = radius * cos(angleRad)
            y = radius * sin(angleRad)
            points.append(App.Vector(x, y, 0))

        # Create Draft objects
        bspline = Draft.makeBSpline(points, closed=False)
        closingLine = Draft.makeLine(points[-1], points[0])
        doc.recompute()

        # Create sketch
        # We do this because Draft.make_wires does not support B-splines
        # and we need a closed wire for the slab
        sketch = Draft.makeSketch([bspline, closingLine], autoconstraints=True, delete=True)
        if sketch is None:
            self.fail("Sketch creation failed")
        sketch.recompute()

        # Create slab
        slab = Arch.makeStructure(sketch, length=extrusionLength, name="Slab")
        slab.recompute()

        # Calculate theoretical areas
        radiusMeters = radius / 1000
        heightMeters = extrusionLength / 1000
        theoreticalHorizontalArea = (pi * radiusMeters**2) / 2
        theoreticalVerticalArea = (pi * radiusMeters + 2 * radiusMeters) * heightMeters

        # Get actual areas
        actualHorizontalArea = slab.HorizontalArea.getValueAs("m^2").Value
        actualVerticalArea = slab.VerticalArea.getValueAs("m^2").Value

        # Optimally wrapped assertions
        self.assertAlmostEqual(
            actualHorizontalArea,
            theoreticalHorizontalArea,
            places=3,
            msg=(
                "Horizontal area > 0.1% tolerance | "
                f"Exp: {theoreticalHorizontalArea:.3f} m² | "
                f"Got: {actualHorizontalArea:.3f} m²"
            ),
        )

        self.assertAlmostEqual(
            actualVerticalArea,
            theoreticalVerticalArea,
            places=3,
            msg=(
                "Vertical area > 0.1% tolerance | "
                f"Exp: {theoreticalVerticalArea:.3f} m² | "
                f"Got: {actualVerticalArea:.3f} m²"
            ),
        )

    def testHouseSpaceAreas(self):
        """Test the HorizontalArea and VerticalArea properties of a house-like space.

        See https://github.com/FreeCAD/FreeCAD/issues/14687.
        """

        operation = "Checking house space area calculation..."
        self.printTestMessage(operation)

        doc = App.ActiveDocument

        # Dimensional parameters (all in mm)
        baseLength = 5000  # 5m along X-axis
        baseWidth = 5000  # 5m along Y-axis (extrusion depth)
        rectangleHeight = 2500  # 2.5m lower rectangular portion
        triangleHeight = 2500  # 2.5m upper triangular portion
        totalHeight = rectangleHeight + triangleHeight  # 5m total height

        # Create envelope profile points (XZ plane)
        points = [
            App.Vector(0, 0, 0),
            App.Vector(baseLength, 0, 0),
            App.Vector(baseLength, 0, rectangleHeight),
            App.Vector(baseLength / 2, 0, totalHeight),
            App.Vector(0, 0, rectangleHeight),
        ]

        # Create wire with automatic face creation
        wire = Draft.makeWire(points, closed=True, face=True)
        if not wire:
            self.fail(f"Wire creation failed with points: {points}\n")
        doc.recompute()

        # Extrude the wire
        extrudedObj = Draft.extrude(wire, App.Vector(0, baseWidth, 0), solid=True)
        if not extrudedObj:
            self.fail("Extrusion failed - no object created\n")
        extrudedObj.Label = "Extruded house"
        doc.recompute()

        # Create Arch Space from the extrusion
        space = Arch.makeSpace(extrudedObj)
        space.Label = "House space"
        doc.recompute()

        # Calculate theoretical areas
        # Horizontal area (only bottom face on XY plane)
        theoreticalHorizontalArea = (baseLength * baseWidth) / 1e6  # 25 m²

        # Vertical areas
        # Side faces (YZ plane) - two rectangles
        sideFaceArea = (rectangleHeight * baseWidth) / 1e6  # 12.5 m² each
        totalSides = sideFaceArea * 2  # 25 m²

        # Front/back faces (XZ plane)
        rectangularPart = (baseLength * rectangleHeight) / 1e6  # 12.5 m²
        triangularPart = (baseLength * triangleHeight / 2) / 1e6  # 6.25 m²
        totalFrontBack = (rectangularPart + triangularPart) * 2  # 37.5 m²

        theoreticalVerticalArea = totalSides + totalFrontBack  # 62.5 m²

        # Get actual areas from space
        actualHorizontalArea = space.HorizontalArea.getValueAs("m^2").Value
        actualVerticalArea = space.VerticalArea.getValueAs("m^2").Value

        self.assertAlmostEqual(
            actualHorizontalArea,
            theoreticalHorizontalArea,
            places=3,
            msg=f"Horizontal area > 0.1% | Exp: {theoreticalHorizontalArea:.3f} | "
            f"Got: {actualHorizontalArea:.3f}",
        )

        self.assertAlmostEqual(
            actualVerticalArea,
            theoreticalVerticalArea,
            places=3,
            msg=f"Vertical area > 0.1% | Exp: {theoreticalVerticalArea:.3f} | "
            f"Got: {actualVerticalArea:.3f}",
        )

    def test_remove_single_window_from_wall_host_is_none(self):
        """
        Tests that a window is removed from its wall's host list when
        Arch.removeComponents is called with host=None (single window selection scenario).

        See https://github.com/FreeCAD/FreeCAD/issues/21551
        """

        # Create a basic wall
        wall_base = Draft.makeLine(App.Vector(0, 0, 0), App.Vector(3000, 0, 0))
        wall = Arch.makeWall(wall_base, width=200, height=2500)

        # Create a window to be added to the wall
        window_width = 1000.0
        window_height = 1200.0

        # Create a Draft Rectangle for the window's Base. Arch.makeWindow can work with
        # either a Wire or a Face.
        window_base = Draft.makeRectangle(length=window_width, height=window_height)

        window = Arch.makeWindow(baseobj=window_base)
        window.Width = window_width  # Manually set as makeWindow(base) doesn't
        window.Height = window_height  # Manually set

        self.document.recompute()

        # Add the window to the wall.
        Arch.addComponents(window, wall)
        self.document.recompute()

        # Pre-condition check: ensure the wall is indeed a host for the window.
        self.assertIn(wall, window.Hosts, "Wall should be in window.Hosts before removal.")
        self.assertEqual(len(window.Hosts), 1, "Window should have 1 host before removal.")

        # Simulate the Arch_Remove command with the scenario where only the window
        # is selected and "Remove" is used.
        Arch.removeComponents([window], host=None)
        self.document.recompute()  # Important for Arch objects to update their state.

        # Assert: the wall should no longer be in the window's Hosts list.
        self.assertNotIn(wall, window.Hosts, "Wall should not be in window.Hosts after removal.")
        self.assertEqual(len(window.Hosts), 0, "Window.Hosts list should be empty after removal.")
