from math import pi, cos, sin
import unittest
from unittest.mock import MagicMock

from nose.tools import eq_

from helix.Services.dxf_helper import DXFHelper
from helix.constants.module_type_constants.dual_tilt_96_cell_constants import DualTilt96CellConstants
from helix.constants.panel_type import PanelType
from helix.constants.system_type import SystemType
from helix.models.coordinate import Coordinate
from helix.models.dxf.dxf_error import DXFError
from helix.models.dxf.graph_direction import GraphDirection
from helix.models.dxf.graph_node import GraphNode
from helix.models.dxf.polygon import Polygon
from helix.models.panel import Panel
from helix.models.subarray import Subarray


class DXFHelperTest(unittest.TestCase):
    def setUp(self):
        self.subject = DXFHelper()

    def test_generate_panels(self):
        modules = [
            Polygon(points=[(-2319.403, 687.080), (-2367.852, 648.146), (-2397.356, 684.863), (-2348.907, 723.792)]),
            Polygon(points=[(-2270.9747, 726.000), (-2319.419, 687.063), (-2348.923, 723.780), (-2300.478, 762.709)])
        ]

        translated_modules = [
            Polygon(points=[(77.953, 38.933), (29.504, 0.0), (0.0, 36.717), (48.449, 75.646)]),
            Polygon(points=[(126.382, 77.850), (77.937, 38.917), (48.433, 75.634), (96.878, 114.563)])
        ]

        panels = self.subject.generate_panels(modules, translated_modules)

        expected = [
            Panel(id=1, coordinate=Coordinate(38.977, 37.824, 38.78204144530477), original_coordinate=Coordinate(-2358.3795, 685.970, 38.78204144530477)),
            Panel(id=2, coordinate=Coordinate(87.407, 76.741, 38.78435126730565), original_coordinate=Coordinate(-2309.949, 724.888, 38.78435126730565))
        ]

        eq_(panels, expected)

    def test_should_consolidate_modules_returns_false_for_single_tilt(self):
        eq_(self.subject.should_consolidate_modules(None, SystemType.singleTilt, None), False)

    def test_should_consolidate_modules_returns_true_for_dual_tilt_that_are_not_consolidated(self):
        modules = [
            Polygon(points=[(-5.17655, -38.8958), (-7.73719, -34.4719), (-4.81221, -32.7789), (-2.25157, -37.2028)]),
            Polygon(points=[(-2.25157, -37.2028), (-5.17655, -38.8958), (1.04417, -35.2963), (-1.88081, -36.9894)]),
        ]
        eq_(self.subject.should_consolidate_modules(modules, SystemType.dualTilt, DualTilt96CellConstants()), True)

    def test_should_consolidate_modules_returns_false_for_dual_tilt_that_are_consolidated(self):
        modules = [
            Polygon(points=[(0.0, 68.795), (48.453, 107.732), (103.732, 38.937), (55.28, 0.0)]),
        ]
        eq_(self.subject.should_consolidate_modules(modules, SystemType.dualTilt, DualTilt96CellConstants()), False)

    def test_consolidate_dual_tilt_modules(self):
        modules = [
            Polygon(points=[(0.0, 68.795), (48.453, 107.732), (76.091, 73.338), (27.634, 34.401)]),
            Polygon(points=[(103.732, 38.937), (55.28, 0.0), (27.642, 34.393), (76.098, 73.331)]),
        ]

        expected_modules = [
            Polygon(points=[(0.0, 68.795), (48.453, 107.732), (103.732, 38.937), (55.28, 0.0)]),
        ]
        module_format = None
        panels = self.subject.consolidate_dual_tilt_modules(modules,
                                                            SystemType.dualTilt,
                                                            module_format)

        eq_(panels, expected_modules)

    def test_consolidate_dual_tilt_modules_does_nothing_if_given_single_tilt_modules(self):
        modules = [
            Polygon(points=[(0.0, 68.795), (48.453, 107.732), (76.091, 73.338), (27.634, 34.401)]),
            Polygon(points=[(103.732, 38.937), (55.28, 0.0), (27.642, 34.393), (76.098, 73.331)]),
        ]

        expected_modules = [
            Polygon(points=[(0.0, 68.795), (48.453, 107.732), (76.091, 73.338), (27.634, 34.401)]),
            Polygon(points=[(103.732, 38.937), (55.28, 0.0), (27.642, 34.393), (76.098, 73.331)]),
        ]
        module_format = None
        panels = self.subject.consolidate_dual_tilt_modules(modules,
                                                            SystemType.singleTilt,
                                                            module_format)

        eq_(panels, expected_modules)

    def test_consolidates_dual_tilt_modules_throws_if_given_dual_tilt_but_unpairable_modules(self):
        modules = [
            Polygon(points=[(0.0, 68.795), (48.453, 107.732), (76.091, 73.338), (27.634, 34.401)]),
            Polygon(points=[(1103.732, 38.937), (155.28, 0.0), (127.642, 34.393), (176.098, 73.331)]),
        ]

        try:
            module_format = None
            self.subject.consolidate_dual_tilt_modules(modules,
                                                       SystemType.dualTilt,
                                                       module_format)
            assert False
        except DXFError as error:
            eq_(error.message, "Error - not a dual tilt file, or invalid dual tilt design.")

    def test_generate_polygons(self):
        lines = [
            MagicMock(start=(-589.128, 174.518), end=(-601.434, 164.629)),
            MagicMock(start=(-601.434, 164.629), end=(-608.928, 173.955)),
            MagicMock(start=(-608.928, 173.955), end=(-596.622, 183.843)),
            MagicMock(start=(-596.622, 183.843), end=(-589.128, 174.518))
        ]
        polygons = self.subject.generate_polygons(lines)

        expected = [Polygon(points=[(-589.128, 174.518), (-601.434, 164.629), (-608.928, 173.955), (-596.622, 183.843)])]
        eq_(polygons, expected)

    def test_generate_polygons_seperate_point(self):
        lines = [
            MagicMock(start=(-589.128, 174.518), end=(-601.434, 164.629)),
            MagicMock(start=(-601.434, 164.629), end=(-608.928, 173.955)),
            MagicMock(start=(1, 2), end=(3, 4)),
            MagicMock(start=(-608.928, 173.955), end=(-596.622, 183.843)),
            MagicMock(start=(-596.622, 183.843), end=(-589.128, 174.518))
        ]
        polygons = self.subject.generate_polygons(lines)

        expected = [
            Polygon(points=[(-589.128, 174.518), (-601.434, 164.629), (-608.928, 173.955)]),
            Polygon(points=[(1, 2), (3, 4)]),
            Polygon(points=[(-608.928, 173.955), (-596.622, 183.843), (-589.128, 174.518)])
        ]

        eq_(polygons, expected)

    def test_build_polygons(self):
        entities = [
            MagicMock(layer='Modules', start=(-589.128, 174.518), end=(-601.434, 164.629)),
            MagicMock(layer='Modules', start=(-601.434, 164.629), end=(-608.928, 173.955)),
            MagicMock(layer='Modules', start=(-608.928, 173.955), end=(-596.622, 183.843)),
            MagicMock(layer='Modules', start=(-596.622, 183.843), end=(-589.128, 174.518)),
            MagicMock(layer='Buildings', start=(-576.827, 184.403), end=(-589.132, 174.514)),
            MagicMock(layer='Buildings', start=(-589.132, 174.514), end=(-596.626, 183.84)),
            MagicMock(layer='Buildings', start=(-596.626, 183.84), end=(-584.321, 193.728)),
            MagicMock(layer='Buildings', start=(-584.321, 193.728), end=(-576.827, 184.403)),
        ]

        polygons = self.subject.build_polygons(entities)

        inches_per_feet = 12
        buildings = [Polygon(points=[(inches_per_feet * -576.827, inches_per_feet * 184.403), (inches_per_feet * -589.132, inches_per_feet * 174.514), (inches_per_feet * -596.626, inches_per_feet * 183.84), (inches_per_feet * -584.321, inches_per_feet * 193.728)])]
        modules = [Polygon(points=[(inches_per_feet * -589.128, inches_per_feet * 174.518), (inches_per_feet * -601.434, inches_per_feet * 164.629), (inches_per_feet * -608.928, inches_per_feet * 173.955), (inches_per_feet * -596.622, inches_per_feet * 183.843)])]

        expected = (buildings, modules)

        eq_(polygons, expected)

    def test_translate_towards_origin(self):
        buildings = [Polygon(points=[(-576.827, 184.403), (-589.132, 174.514), (-596.626, 183.84), (-584.321, 193.728)])]
        modules = [Polygon(points=[(-589.128, 174.518), (-601.434, 164.629), (-608.928, 173.955), (-596.622, 183.843)])]

        expected_buildings = [Polygon(points=[(32.101, 19.774), (19.796, 9.885), (12.302, 19.211), (24.607, 29.099)])]
        expected_modules = [Polygon(points=[(19.8, 9.889), (7.494, 0), (0, 9.326), (12.306, 19.214)])]

        eq_(self.subject.translate_towards_origin(buildings, modules), (expected_buildings, expected_modules))

    def test_get_polygons_counterclockwise(self):

        polygons = [Polygon(points=[(0, 0), (0, 1), (1, 1), (1, 0)]),
                    Polygon(points=[(0, 0), (0, 2), (2, 2), (2, 0)]),
                    Polygon(points=[(0, 0), (2, 0), (2, 2), (0, 2)])]

        expected_polygons = [Polygon(points=[(1, 0), (1, 1), (0, 1), (0, 0)]),
                             Polygon(points=[(2, 0), (2, 2), (0, 2), (0, 0)]),
                             Polygon(points=[(0, 0), (2, 0), (2, 2), (0, 2)])]

        eq_(self.subject.get_polygons_counterclockwise(polygons), expected_polygons)

    def test_build_node_graph(self):
        panels = [
            Panel(coordinate=Coordinate(10, 0, rotation=0)),
            Panel(coordinate=Coordinate(20, 0, rotation=0)),
            Panel(coordinate=Coordinate(10, 20, rotation=0)),
            Panel(coordinate=Coordinate(20, 20, rotation=0))
        ]
        node_graph = self.subject.build_node_graph(panels, (10, 10))

        node_a = GraphNode(Panel(coordinate=Coordinate(10, 0, rotation=0)), 10, 10)
        node_b = GraphNode(Panel(coordinate=Coordinate(20, 0, rotation=0)), 10, 10)
        node_c = GraphNode(Panel(coordinate=Coordinate(10, 20, rotation=0)), 10, 10)
        node_d = GraphNode(Panel(coordinate=Coordinate(20, 20, rotation=0)), 10, 10)

        node_a.add_neighbor(node_b, GraphDirection.East)
        node_c.add_neighbor(node_d, GraphDirection.East)

        expected = [node_a, node_b, node_c, node_d]
        eq_(node_graph, expected)

    def test_build_node_graph_weird_case(self):
        panels = [
            Panel(coordinate=Coordinate(38.04431188249998, 857.508730771, -51.21962134154259)),
            Panel(coordinate=Coordinate(86.50299946749976, 896.449696681, -51.21563589970276))
        ]

        node_graph = self.subject.build_node_graph(panels, (88.24, 62.16))

        node_a = GraphNode(Panel(coordinate=Coordinate(38.04431188249998, 857.508730771, -51.21962134154259)), 88.24, 62.16)
        node_b = GraphNode(Panel(coordinate=Coordinate(86.50299946749976, 896.449696681, -51.21563589970276)), 88.24, 62.16)

        node_a.add_neighbor(node_b, GraphDirection.North)

        eq_(node_graph, [node_a, node_b])

    def test_build_node_graph_throws_dxf_validation_error_if_there_are_panels_with_no_neighbors(self):
        panels = [
            Panel(coordinate=Coordinate(10, 0, rotation=0)),
            Panel(coordinate=Coordinate(20, 0, rotation=0)),
            Panel(coordinate=Coordinate(100, 200, rotation=0))
        ]
        try:
            self.subject.build_node_graph(panels, (10, 10))
            assert False
        except DXFError as error:
            eq_(error.message, "Error - invalid module spacing. Please check to make sure the correct system type and panel spacing are present")

    def test_detect_subarrays(self):
        node_a = GraphNode(Panel(coordinate=Coordinate(1, 0, rotation=0)), 1, 1)
        node_b = GraphNode(Panel(coordinate=Coordinate(2, 0, rotation=0)), 1, 1)
        node_c = GraphNode(Panel(coordinate=Coordinate(1, 2, rotation=0)), 1, 1)

        node_a.add_neighbor(node_b, GraphDirection.East)

        nodes = [node_a, node_b, node_c]
        panels = [node.panel for node in nodes]

        subarrays = self.subject.detect_subarrays(nodes, panels)
        expected = [
            Subarray(subarray_number=1, start_row=0, size=2),
            Subarray(subarray_number=2, start_row=2, size=1)
        ]

        eq_(subarrays, expected)

    def test_throws_dxf_validation_error_if_blows_stack_during_subarray_detection(self):
        nodes = []
        for i in range(3000):
            nodes.append(GraphNode(Panel(coordinate=Coordinate(i, 0)), i, 0))

        for i in range(1, 3000):
            node_a = nodes[i - 1]
            node = nodes[i]
            node.add_neighbor(node_a, GraphDirection.East)

        panels = [node.panel for node in nodes]
        try:
            subarrays = self.subject.detect_subarrays(nodes, panels)
            eq_(len(subarrays), 0)
        except DXFError as error:
            eq_(error.message, "Array size is too big. Max is 150' by 150'.")

    def test_detect_panel_types(self):
        node_0_0 = GraphNode(Panel(coordinate=Coordinate(0, 0, rotation=0)), 1, 1)
        node_1_0 = GraphNode(Panel(coordinate=Coordinate(1, 0, rotation=0)), 1, 1)
        node_2_0 = GraphNode(Panel(coordinate=Coordinate(2, 0, rotation=0)), 1, 1)
        node_3_0 = GraphNode(Panel(coordinate=Coordinate(3, 0, rotation=0)), 1, 1)

        node_0_1 = GraphNode(Panel(coordinate=Coordinate(0, 1, rotation=0)), 1, 1)
        node_1_1 = GraphNode(Panel(coordinate=Coordinate(1, 1, rotation=0)), 1, 1)
        node_2_1 = GraphNode(Panel(coordinate=Coordinate(2, 1, rotation=0)), 1, 1)
        node_3_1 = GraphNode(Panel(coordinate=Coordinate(3, 1, rotation=0)), 1, 1)

        node_0_2 = GraphNode(Panel(coordinate=Coordinate(0, 2, rotation=0)), 1, 1)
        node_1_2 = GraphNode(Panel(coordinate=Coordinate(1, 2, rotation=0)), 1, 1)
        node_2_2 = GraphNode(Panel(coordinate=Coordinate(2, 2, rotation=0)), 1, 1)

        node_0_0.add_neighbor(node_0_1, GraphDirection.North)
        node_0_0.add_neighbor(node_1_1, GraphDirection.NorthEast)
        node_0_0.add_neighbor(node_1_0, GraphDirection.East)

        node_2_0.add_neighbor(node_1_0, GraphDirection.West)
        node_2_0.add_neighbor(node_1_1, GraphDirection.NorthWest)
        node_2_0.add_neighbor(node_2_1, GraphDirection.North)
        node_2_0.add_neighbor(node_3_1, GraphDirection.NorthEast)
        node_2_0.add_neighbor(node_3_0, GraphDirection.East)

        node_1_1.add_neighbor(node_0_1, GraphDirection.West)
        node_1_1.add_neighbor(node_1_0, GraphDirection.South)
        node_1_1.add_neighbor(node_1_2, GraphDirection.North)
        node_1_1.add_neighbor(node_2_1, GraphDirection.East)

        node_3_1.add_neighbor(node_2_1, GraphDirection.West)

        node_0_2.add_neighbor(node_0_1, GraphDirection.South)
        node_0_2.add_neighbor(node_1_1, GraphDirection.SouthEast)
        node_0_2.add_neighbor(node_1_2, GraphDirection.East)

        node_2_2.add_neighbor(node_1_2, GraphDirection.West)
        node_2_2.add_neighbor(node_1_1, GraphDirection.SouthWest)
        node_2_2.add_neighbor(node_2_1, GraphDirection.South)
        node_2_2.add_neighbor(node_3_1, GraphDirection.SouthEast)

        nodes = [
            node_0_0,
            node_1_0,
            node_2_0,
            node_3_0,
            node_0_1,
            node_1_1,
            node_2_1,
            node_3_1,
            node_0_2,
            node_1_2,
            node_2_2
        ]

        self.subject.detect_panel_types(nodes)

        def assert_has_type(node, panel_type):
            eq_(node.panel.panel_type, panel_type)

        assert_has_type(node_0_0, PanelType.Corner)
        assert_has_type(node_1_0, PanelType.NorthSouth)
        assert_has_type(node_2_0, PanelType.NorthSouth)
        assert_has_type(node_3_0, PanelType.Corner)
        assert_has_type(node_0_1, PanelType.EastWest)
        assert_has_type(node_1_1, PanelType.Middle)
        assert_has_type(node_2_1, PanelType.Middle)
        assert_has_type(node_3_1, PanelType.Corner)
        assert_has_type(node_0_2, PanelType.Corner)
        assert_has_type(node_1_2, PanelType.NorthSouth)
        assert_has_type(node_2_2, PanelType.Corner)

    def test_detect_dual_tilt_wind_zones_simple_rectangle_mid_lb(self):
        system_type = SystemType.dualTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        buildings = [
            Polygon(points=[
                (-0.5, -0.5),
                (3.5, -0.5),
                (3.5, 2.5),
                (-0.5, 2.5)
            ])
        ]

        l_b = 0.55
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            eq_(panel.wind_zone, wind_zone)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 0)
        assert_has_wind_zone(panel_1_0, 1)
        assert_has_wind_zone(panel_2_0, 1)
        assert_has_wind_zone(panel_3_0, 0)

        assert_has_wind_zone(panel_0_1, 1)
        assert_has_wind_zone(panel_1_1, 2)
        assert_has_wind_zone(panel_2_1, 2)
        assert_has_wind_zone(panel_3_1, 1)

        assert_has_wind_zone(panel_0_2, 0)
        assert_has_wind_zone(panel_1_2, 1)
        assert_has_wind_zone(panel_2_2, 1)

    def test_detect_dual_tilt_wind_zones_simple_rectangle_high_lb(self):
        system_type = SystemType.dualTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        buildings = [
            Polygon(points=[
                (-0.5, -0.5),
                (3.5, -0.5),
                (3.5, 2.5),
                (-0.5, 2.5)
            ])
        ]

        def assert_has_wind_zone(panel, wind_zone):
            eq_(panel.wind_zone, wind_zone)

        l_b = 1
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        assert_has_wind_zone(panel_0_0, 0)
        assert_has_wind_zone(panel_1_0, 0)
        assert_has_wind_zone(panel_2_0, 0)
        assert_has_wind_zone(panel_3_0, 0)

        assert_has_wind_zone(panel_0_1, 0)
        assert_has_wind_zone(panel_1_1, 1)
        assert_has_wind_zone(panel_2_1, 1)
        assert_has_wind_zone(panel_3_1, 0)

        assert_has_wind_zone(panel_0_2, 0)
        assert_has_wind_zone(panel_1_2, 0)
        assert_has_wind_zone(panel_2_2, 0)

    def test_detect_dual_tilt_wind_zones_simple_rectangle_low_lb(self):
        system_type = SystemType.dualTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        buildings = [
            Polygon(points=[
                (-0.5, -0.5),
                (3.5, -0.5),
                (3.5, 2.5),
                (-0.5, 2.5)
            ])
        ]

        def assert_has_wind_zone(panel, wind_zone):
            eq_(panel.wind_zone, wind_zone)
        l_b = 0.3
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 1)
        assert_has_wind_zone(panel_1_0, 2)
        assert_has_wind_zone(panel_2_0, 2)
        assert_has_wind_zone(panel_3_0, 1)

        assert_has_wind_zone(panel_0_1, 2)
        assert_has_wind_zone(panel_1_1, 3)
        assert_has_wind_zone(panel_2_1, 3)
        assert_has_wind_zone(panel_3_1, 2)

        assert_has_wind_zone(panel_0_2, 1)
        assert_has_wind_zone(panel_1_2, 2)
        assert_has_wind_zone(panel_2_2, 2)

        # try again with a lower l_B
        l_b = 0.3
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            eq_(panel.wind_zone, wind_zone)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 1)
        assert_has_wind_zone(panel_1_0, 2)
        assert_has_wind_zone(panel_2_0, 2)
        assert_has_wind_zone(panel_3_0, 1)

        assert_has_wind_zone(panel_0_1, 2)
        assert_has_wind_zone(panel_1_1, 3)
        assert_has_wind_zone(panel_2_1, 3)
        assert_has_wind_zone(panel_3_1, 2)

        assert_has_wind_zone(panel_0_2, 1)
        assert_has_wind_zone(panel_1_2, 2)
        assert_has_wind_zone(panel_2_2, 2)

    def test_detect_dual_tilt_wind_zones_simple_rectangle_very_low_lb(self):
        system_type = SystemType.dualTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        buildings = [
            Polygon(points=[
                (-0.5, -0.5),
                (3.5, -0.5),
                (3.5, 2.5),
                (-0.5, 2.5)
            ])
        ]

        def assert_has_wind_zone(panel, wind_zone):
            eq_(panel.wind_zone, wind_zone)
        l_b = 0.2
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 2)
        assert_has_wind_zone(panel_1_0, 3)
        assert_has_wind_zone(panel_2_0, 3)
        assert_has_wind_zone(panel_3_0, 2)

        assert_has_wind_zone(panel_0_1, 3)
        assert_has_wind_zone(panel_1_1, 4)
        assert_has_wind_zone(panel_2_1, 4)
        assert_has_wind_zone(panel_3_1, 3)

        assert_has_wind_zone(panel_0_2, 2)
        assert_has_wind_zone(panel_1_2, 3)
        assert_has_wind_zone(panel_2_2, 3)

        # try again with a lower l_B
        l_b = 0.3
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            eq_(panel.wind_zone, wind_zone)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 1)
        assert_has_wind_zone(panel_1_0, 2)
        assert_has_wind_zone(panel_2_0, 2)
        assert_has_wind_zone(panel_3_0, 1)

        assert_has_wind_zone(panel_0_1, 2)
        assert_has_wind_zone(panel_1_1, 3)
        assert_has_wind_zone(panel_2_1, 3)
        assert_has_wind_zone(panel_3_1, 2)

        assert_has_wind_zone(panel_0_2, 1)
        assert_has_wind_zone(panel_1_2, 2)
        assert_has_wind_zone(panel_2_2, 2)

    def test_detect_single_tilt_wind_zones_simple_rectangle_mid_lb(self):
        system_type = SystemType.singleTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')
        panel_3_2 = Panel(coordinate=Coordinate(3, 2, rotation=0), id='3_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2, panel_3_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        buildings = [
            Polygon(points=[
                (-0.5, -0.5),
                (3.5, -0.5),
                (3.5, 2.5),
                (-0.5, 2.5)
            ])
        ]

        l_b = 0.55
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            assert wind_zone == panel.wind_zone, "Expected panel to be in wind_zone %d, got %d" % (wind_zone, panel.wind_zone)
            eq_(panel.wind_zone, wind_zone)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 8)
        assert_has_wind_zone(panel_1_0, 7)
        assert_has_wind_zone(panel_2_0, 7)
        assert_has_wind_zone(panel_3_0, 8)

        assert_has_wind_zone(panel_0_1, 9)
        assert_has_wind_zone(panel_1_1, 2)
        assert_has_wind_zone(panel_2_1, 2)
        assert_has_wind_zone(panel_3_1, 9)

        assert_has_wind_zone(panel_0_2, 0)
        assert_has_wind_zone(panel_1_2, 2)
        assert_has_wind_zone(panel_2_2, 2)
        assert_has_wind_zone(panel_3_2, 0)

    def test_detect_single_tilt_wind_zones_simple_rectangle_low_lb(self):
        system_type = SystemType.singleTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')
        panel_3_2 = Panel(coordinate=Coordinate(3, 2, rotation=0), id='3_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2, panel_3_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        buildings = [
            Polygon(points=[
                (-0.5, -0.5),
                (3.5, -0.5),
                (3.5, 2.5),
                (-0.5, 2.5)
            ])
        ]

        l_b = 0.35
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            assert wind_zone == panel.wind_zone, "Expected panel to be in wind_zone %d, got %d" % (wind_zone, panel.wind_zone)
            eq_(panel.wind_zone, wind_zone)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E, 5 is F, 6 is G, 7 is H, 8 is I, 9 is J, 10 is K
        # A, I, H, B, J, C, E, F, G, D, K (order of most uplift force

        assert_has_wind_zone(panel_0_0, 9)
        assert_has_wind_zone(panel_1_0, 4)
        assert_has_wind_zone(panel_2_0, 4)
        assert_has_wind_zone(panel_3_0, 9)

        assert_has_wind_zone(panel_0_1, 2)
        assert_has_wind_zone(panel_1_1, 5)
        assert_has_wind_zone(panel_2_1, 5)
        assert_has_wind_zone(panel_3_1, 2)

        assert_has_wind_zone(panel_0_2, 0)
        assert_has_wind_zone(panel_1_2, 3)
        assert_has_wind_zone(panel_2_2, 3)
        assert_has_wind_zone(panel_3_2, 0)

    def test_detect_dual_tilt_wind_zones_10_gon_building(self):
        system_type = SystemType.dualTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        points = []
        radius = 3
        building_vertices = 10
        for step in range(building_vertices, 0, -1):  # create a 10-gon
            angle = (2 * pi / building_vertices) * step
            x = radius * sin(angle)
            y = radius * cos(angle)
            points.append((x, y))

        buildings = [Polygon(points=points)]

        l_b = 2
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            eq_(panel.wind_zone, wind_zone)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 4)
        assert_has_wind_zone(panel_1_0, 4)
        assert_has_wind_zone(panel_2_0, 4)
        assert_has_wind_zone(panel_3_0, 4)

        assert_has_wind_zone(panel_0_1, 4)
        assert_has_wind_zone(panel_1_1, 4)
        assert_has_wind_zone(panel_2_1, 4)
        assert_has_wind_zone(panel_3_1, 4)

        assert_has_wind_zone(panel_0_2, 4)
        assert_has_wind_zone(panel_1_2, 4)
        assert_has_wind_zone(panel_2_2, 4)

    def test_detect_dual_tilt_wind_zones_hexagon_building(self):
        system_type = SystemType.dualTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        points = []
        radius = 4
        building_vertices = 6
        for step in range(building_vertices, 0, -1):  # create a 10-gon
            angle = (2 * pi / building_vertices) * step
            x = radius * sin(angle)
            y = radius * cos(angle)
            points.append((x + 1, y + 1))

        buildings = [Polygon(points=points)]

        l_b = 1
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            assert panel.wind_zone == wind_zone, "expected panel to be in wind zone %d, got %d" % (wind_zone, panel.wind_zone)

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 1)
        assert_has_wind_zone(panel_1_0, 2)
        assert_has_wind_zone(panel_2_0, 1)
        assert_has_wind_zone(panel_3_0, 1)

        assert_has_wind_zone(panel_0_1, 2)
        assert_has_wind_zone(panel_1_1, 2)
        assert_has_wind_zone(panel_2_1, 2)
        assert_has_wind_zone(panel_3_1, 1)

        assert_has_wind_zone(panel_0_2, 1)
        assert_has_wind_zone(panel_1_2, 2)
        assert_has_wind_zone(panel_2_2, 1)

    def test_detect_dual_tilt_wind_zones_triangle_building(self):
        system_type = SystemType.dualTilt

        panel_0_0 = Panel(coordinate=Coordinate(0, 0, rotation=0), id='0_0')
        panel_1_0 = Panel(coordinate=Coordinate(1, 0, rotation=0), id='1_0')
        panel_2_0 = Panel(coordinate=Coordinate(2, 0, rotation=0), id='2_0')
        panel_3_0 = Panel(coordinate=Coordinate(3, 0, rotation=0), id='3_0')
        panel_0_1 = Panel(coordinate=Coordinate(0, 1, rotation=0), id='0_1')
        panel_1_1 = Panel(coordinate=Coordinate(1, 1, rotation=0), id='1_1')
        panel_2_1 = Panel(coordinate=Coordinate(2, 1, rotation=0), id='2_1')
        panel_3_1 = Panel(coordinate=Coordinate(3, 1, rotation=0), id='3_1')
        panel_0_2 = Panel(coordinate=Coordinate(0, 2, rotation=0), id='0_2')
        panel_1_2 = Panel(coordinate=Coordinate(1, 2, rotation=0), id='1_2')
        panel_2_2 = Panel(coordinate=Coordinate(2, 2, rotation=0), id='2_2')

        panels = [
            panel_0_2, panel_1_2, panel_2_2,
            panel_0_1, panel_1_1, panel_2_1, panel_3_1,
            panel_0_0, panel_1_0, panel_2_0, panel_3_0,
        ]

        sorted_panels = sorted(panels, key=lambda x: x.id)

        modules = [Polygon(points=[(p.coordinate.x, p.coordinate.y)]) for p in sorted_panels]

        points = []
        radius = 4
        building_vertices = 3
        for step in range(building_vertices, 0, -1):  # create a 10-gon
            angle = (2 * pi / building_vertices) * step
            x = radius * sin(angle)
            y = radius * cos(angle)
            points.append((x + 1, y + 1))

        buildings = [Polygon(points=points)]

        l_b = 1
        self.subject.detect_wind_zones(panels, buildings, modules, l_b, system_type)

        def assert_has_wind_zone(panel, wind_zone):
            assert panel.wind_zone == wind_zone, "expected panel to be in wind zone %d, got %d" % (wind_zone, panel.wind_zone)
            assert panel.fuzzy_wind_zone, "expected panel to be in a fuzzy wind zone"

        # 0 is A, 1 is B, 2 is C, 3 is D, 4 is E

        assert_has_wind_zone(panel_0_0, 1)
        assert_has_wind_zone(panel_1_0, 1)
        assert_has_wind_zone(panel_2_0, 1)
        assert_has_wind_zone(panel_3_0, 0)

        assert_has_wind_zone(panel_0_1, 1)
        assert_has_wind_zone(panel_1_1, 2)
        assert_has_wind_zone(panel_2_1, 1)
        assert_has_wind_zone(panel_3_1, 1)

        assert_has_wind_zone(panel_0_2, 1)
        assert_has_wind_zone(panel_1_2, 1)
        assert_has_wind_zone(panel_2_2, 1)

    def test_compute_corner_directions(self):
        data = [
            {
                'building_points': [(0, 0), (10, 0), (10, 10), (0, 10)],
                'expected_corner_dirs': [(False, False), (False, True), (True, True), (True, False)]
            },
            {
                'building_points': [(0, 0), (10, 0), (2, 10)],
                'expected_corner_dirs': [(False, False), (False, True), (True, False)]
            },
            {
                'building_points': [(0, 0), (10, 0), (7, 10)],
                'expected_corner_dirs': [(False, False), (False, True), (True, True)]
            },
            {
                'building_points': [(0, 0), (10, 0), (7, 2)],
                'expected_corner_dirs': [(False, False), (False, True), (True, False)]
            },
            {
                'building_points': [(0, 0), (3, -10), (10, 0)],
                'expected_corner_dirs': [(True, False), (False, False), (True, True)]
            },
            {
                'building_points': [(-1, -1), (1, 4), (0, 10)],
                'expected_corner_dirs': [(False, False), (False, True), (True, False)]
            },
            {
                'building_points': [(1, -1), (0, 10), (-1, 4)],
                'expected_corner_dirs': [(False, True), (True, True), (False, False)]
            },
            {
                'building_points': [(0, 1), (4, 0), (8, 2)],
                'expected_corner_dirs': [(True, False), (False, True), (True, True)]
            },
            {
                'building_points': [(0, 2), (4, 0), (8, 1)],
                'expected_corner_dirs': [(True, False), (False, False), (True, True)]
            }
        ]

        for item in data:
            building = item['building_points']
            expected = item['expected_corner_dirs']

            corners = []

            for i in range(len(building)):
                corners.append((building[i - 1], building[i], building[(i + 1) % len(building)]))

            for expected, (prev, vertex, next) in zip(expected, corners):
                print('given:', self.subject.compute_corner_directions(vertex, prev, next, 0))
                assert self.subject.compute_corner_directions(vertex, prev, next, 0) == expected
