"""Opening hours parsers, on the operator pages as read on 11 Sep 2026 (Stream G, G1).

Pure: each fixture is the fragment of the page the parser reads, nothing fetched. What each pins:
the stores a page lists, their terminals and clocks, that a card which is not duty free is left
out, that a closed store's archived page is not followed, and the one line the airport page
prints from a reading, which is deterministic so a changed line means the page changed.
"""

import pathlib

from app.services.hours import base
from app.services.hours.base import StoreHours, parse_timing, summarise, terminal_label
from app.services.hours.dublin import DublinAirportHours
from app.services.hours.heathrow import HeathrowHours
from app.services.hours.registry import HOURS_COLLECTORS, collected_airports, collector_for
from app.services.hours.toronto import TorontoPearsonHours

FIXTURES = pathlib.Path(__file__).resolve().parent / "fixtures"
STORE_PAGE = "https://www.torontopearson.com/en/while-you-are-here/toronto-airport-shops/duty-free-t1-after-intl-e76"


def read(name: str) -> str:
    return (FIXTURES / name).read_text()


class TestTiming:
    def test_the_operators_spellings_read_as_one_clock(self):
        assert parse_timing("Mo-Su 05:30-22:00") == ("05:30-22:00", "daily", None)
        assert parse_timing("Mon-Sun 04:00-22:00 (01) 844 7572") == ("04:00-22:00", "daily", None)
        assert parse_timing("Mon - Fri: 6:00 - 21:00") == ("06:00-21:00", "Mon-Fri", None)
        assert parse_timing("Daily 07.00-23.00") == ("07:00-23:00", "daily", None)

    def test_the_twelve_hour_clock_reads_as_one_clock_too(self):
        """Toronto's boutique pages (run of 11 Sep) print "Every day from 4:00 am to 12:00
        midnight"; the first parser kept that as a rule and a boutique's name led the line."""
        assert parse_timing("Every day from 4:00 am to 12:00 midnight") == ("04:00-00:00", "daily", None)
        assert parse_timing("Every day from 4:00 am to 1:30 am") == ("04:00-01:30", "daily", None)
        assert parse_timing("Mon-Fri 6 a.m. - 9:30 p.m.") == ("06:00-21:30", "Mon-Fri", None)
        assert parse_timing("12 noon to 8 pm") == ("12:00-20:00", None, None)

    def test_a_rule_or_all_day_is_kept_as_a_statement_and_nothing_is_guessed(self):
        assert parse_timing("Open 24 hours") == (None, "daily", "24 hours")
        assert parse_timing("Open 3 hours before flights.") == (None, None, "Open 3 hours before flights.")
        assert parse_timing("") == (None, None, None)

    def test_terminal_labels_fold(self):
        assert terminal_label("Terminal 1") == "T1" and terminal_label("T2") == "T2"
        assert terminal_label(" Terminal 5 ") == "T5" and terminal_label(None) is None


class TestHeathrow:
    def test_fourteen_stores_across_four_terminals_with_their_clocks(self):
        collector = HeathrowHours()
        stores = collector.parse(read("hours_heathrow.html"), collector.pages("LHR")[0])
        assert len(stores) == 14
        assert [s.terminal for s in stores].count("T2") == 4 and {s.terminal for s in stores} == {"T2", "T3", "T4", "T5"}
        assert stores[0] == StoreHours(terminal="T2", area="Departures", times="05:30-22:00", days="daily", statement=None, name="World Duty Free")
        # The T4 gates line carries an entity; the clock differs by store.
        assert StoreHours(terminal="T4", area="Gates 7&8", times="05:30-22:00", days="daily", statement=None, name="World Duty Free") in stores
        assert StoreHours(terminal="T3", area="Departures", times="04:30-22:00", days="daily", statement=None, name="World Duty Free") in stores

    def test_the_line_names_the_departures_store_per_terminal_and_the_rest_as_a_range(self):
        collector = HeathrowHours()
        text = summarise(collector.parse(read("hours_heathrow.html"), ""))
        assert text == (
            "World Duty Free, 14 stores across 4 terminals, every day. "
            "T2 Departures 05:30-22:00, T3 Departures 04:30-22:00, T4 Gate 1A 05:30-22:00, T5 Departures 05:00-22:00; "
            "the other 10 stores open 05:00 to 06:00, closing 22:00."
        )
        assert "—" not in text

    def test_no_operator_prose_in_the_fixture(self):
        """The legal posture: facts only. The page's paragraph about the retailer is not kept."""
        assert "17,000" not in read("hours_heathrow.html")


class TestDublin:
    def test_only_the_duty_free_cards_and_once_each(self):
        collector = DublinAirportHours()
        stores = collector.parse(read("hours_dublin.html"), collector.pages("DUB")[0])
        assert stores == [
            StoreHours(terminal="T1", area=None, times="04:00-22:00", days="daily", statement=None, name="Dublin Airport Duty Free"),
            StoreHours(terminal="T2", area=None, times="04:00-21:00", days="daily", statement=None, name="Dublin Airport Duty Free"),
            StoreHours(terminal="T1", area=None, times="04:00-21:00", days="daily", statement=None, name="WHSmith - Duty Free"),
            StoreHours(terminal="T2", area=None, times="04:00-21:00", days="daily", statement=None, name="WHSmith - Duty Free"),
        ]
        assert "Boots" in read("hours_dublin.html")  # on the page, and not a duty free store

    def test_two_names_get_a_sentence_each(self):
        """The first run against the live page (11 Sep) folded the WHSmith duty free under the
        main store's name; a name that differs is its own sentence."""
        collector = DublinAirportHours()
        assert summarise(collector.parse(read("hours_dublin.html"), "")) == (
            "4 duty free stores across 2 terminals, every day. "
            "Dublin Airport Duty Free: T1 04:00-22:00, T2 04:00-21:00. WHSmith - Duty Free: T1 04:00-21:00, T2 04:00-21:00."
        )


class TestTorontoPearson:
    def test_a_store_page_reads_the_rule_and_where_the_store_is(self):
        collector = TorontoPearsonHours()
        stores = collector.parse(read("hours_toronto.html"), STORE_PAGE)
        assert stores == [StoreHours(
            terminal="T1", area="After security (International), Near Gate E76", times=None, days=None,
            statement="Open 3 hours before flights. Closed with gaps of more than 3 hours between flights.",
            name="Duty Free by Dufry",
        )]

    def test_the_sitemap_is_followed_to_the_open_duty_free_stores_only(self):
        collector = TorontoPearsonHours()
        sitemap = collector.pages("YYZ")[0]
        assert sitemap.endswith("/sitemap_yyz_en.xml")
        followed = collector.follow(read("hours_toronto_sitemap.xml"), sitemap)
        assert followed == [
            "https://www.torontopearson.com/en/while-you-are-here/toronto-airport-shops/duty-free-liquor-t1-after-us-f61",
            "https://www.torontopearson.com/en/while-you-are-here/toronto-airport-shops/duty-free-t1-after-intl-e76",
            "https://www.torontopearson.com/en/while-you-are-here/toronto-airport-shops/duty-free-t3-after-intl-b22",
        ]
        # The sitemap itself holds no store; a store page is not followed further.
        assert collector.parse(read("hours_toronto_sitemap.xml"), sitemap) == []
        assert collector.follow(read("hours_toronto.html"), STORE_PAGE) == []

    def test_a_time_item_whose_content_opens_with_an_empty_div_still_reads(self):
        """Eight of eleven store pages read as having no hours on 11 Sep: the capture stopped at
        the first inner closing div. The whole item is read now."""
        collector = TorontoPearsonHours()
        page = read("hours_toronto.html").replace(
            "Open 3 hours before flights. Closed with gaps of more than 3 hours between flights.<br />\n<div>&nbsp;</div>",
            "<div>&nbsp;</div><p>Every day from 4:00 am to 12:00 midnight</p>",
        )
        assert "<div>&nbsp;</div><p>Every day" in page
        stores = collector.parse(page, STORE_PAGE)
        assert stores[0].times == "04:00-00:00" and stores[0].days == "daily" and stores[0].statement is None

    def test_boutiques_under_the_duty_free_banner_keep_their_own_names(self):
        rule = "Open 3 hours before flights. Closed with gaps of more than 3 hours between flights."
        stores = [
            StoreHours(terminal="T1", area="Near Gate E76", statement=rule, name="Duty Free by Dufry"),
            StoreHours(terminal="T3", area="Near Gate B22", statement=rule, name="Duty Free by Dufry"),
            StoreHours(terminal="T1", area="Near Gate E76", times="04:00-00:00", days="daily", name="Gucci"),
        ]
        assert summarise(stores) == (
            "3 duty free stores across 2 terminals, every day. "
            "Duty Free by Dufry: open 3 hours before flights. Closed with gaps of more than 3 hours between flights. "
            "Gucci: T1 Near Gate E76 04:00-00:00."
        )

    def test_one_rule_shared_by_every_store_is_printed_once(self):
        collector = TorontoPearsonHours()
        one = collector.parse(read("hours_toronto.html"), STORE_PAGE)[0]
        two = StoreHours(terminal="T3", area="After security (International), Near Gate B22",
                         statement=one.statement, name=one.name)
        assert summarise([one, two]) == (
            "Duty Free by Dufry, 2 stores across 2 terminals: open 3 hours before flights. "
            "Closed with gaps of more than 3 hours between flights."
        )
        assert summarise([one]) == (
            "Duty Free by Dufry, 1 store, T1 After security (International), Near Gate E76: "
            "open 3 hours before flights. Closed with gaps of more than 3 hours between flights."
        )


class TestSummaryRules:
    def test_nothing_read_is_an_empty_line_not_a_guess(self):
        assert summarise([]) == ""

    def test_a_terminal_without_a_departures_store_names_its_first_store(self):
        stores = [
            StoreHours(terminal="T1", area="Gate 12", times="06:00-21:00", days="daily", name="Shop"),
            StoreHours(terminal="T1", area="Gate 30", times="07:00-20:00", days="daily", name="Shop"),
            StoreHours(terminal="T2", area="Departures", times="05:00-22:00", days="Mon-Fri", name="Shop"),
        ]
        # Not every store is daily, so no "every day"; the T1 main is Gate 12; the rest is a range.
        assert summarise(stores) == (
            "Shop, 3 stores across 2 terminals. T1 Gate 12 06:00-21:00, T2 Departures 05:00-22:00; "
            "the other 1 store open 07:00, closing 20:00."
        )

    def test_the_registry_covers_exactly_the_platforms_that_read(self):
        assert collected_airports() == ["DUB", "LHR", "YYZ"]
        assert collector_for("lhr").slug == "heathrow-hours" and collector_for("ATH") is None
        assert len({c.slug for c in HOURS_COLLECTORS}) == len(HOURS_COLLECTORS)
        for c in HOURS_COLLECTORS:
            assert c.homepage.startswith("https://") and c.parser_version and c.operator

    def test_a_challenge_page_is_recognised_on_content(self):
        """Paris answered HTTP 200 with an Incapsula frame on 11 Sep, and Toronto Pearson a
        "Radware Captcha Page" after the second read of its store pages; the fetch port let
        both through as pages. A block is a refusal, recorded, never worked around."""
        assert base.looks_challenged('<html><body><iframe>Request unsuccessful. Incapsula incident ID: 1</iframe></body></html>')
        assert base.looks_challenged('<html><head><title>Radware Captcha Page</title></head><body><h1>We apologize for the inconvenience...</h1></body></html>')
        assert not base.looks_challenged(read("hours_dublin.html"))
        assert not base.looks_challenged(read("hours_heathrow.html"))

    def test_the_pace_is_the_slowest_of_theirs_ours_and_the_platforms_own(self):
        from app.services.collectors.robots import Robots

        toronto = TorontoPearsonHours()
        assert toronto.delay_seconds == 10.0
        assert base.pace_for(toronto, Robots.unrestricted("h"), 1.5) == 10.0
        assert base.pace_for(toronto, Robots(host="h", disallows=[], crawl_delay=30.0), 1.5) == 30.0
        assert base.pace_for(HeathrowHours(), Robots.unrestricted("h"), 1.5) == 1.5
