"""Tests for tondortools.tool — pure utility functions."""
from datetime import datetime
from pathlib import Path

import pytest

from tondortools.tool import (
    compile_sentinel2level2a_glob,
    generate_interval_by_span,
    generate_timeperiod_monthly,
    generate_timeperiod_quaterly,
    generate_yearmonth_monthbin,
    generate_yearmonth_monthly,
    generate_yearmonth_quaterly,
    get_input_param,
    get_next_month,
    has_subset_in_string,
    month_range,
    parse_sentinel2_name,
    quarter_range,
    return_eodata_tilename,
    return_tilename,
    set_input_param,
    yearmonth_parse,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _params(*idents_values):
    """Build a minimal job_step_params dict with the given (ident, value) pairs."""
    return {"parameters": [{"ident": i, "value": v} for i, v in idents_values]}


# ---------------------------------------------------------------------------
# get_input_param / set_input_param
# ---------------------------------------------------------------------------

class TestGetInputParam:
    def test_returns_existing_value(self):
        params = _params(("year", 2023), ("sitecode", "DEABC"))
        assert get_input_param(params, "year") == 2023

    def test_returns_string_value(self):
        params = _params(("sitecode", "DEABC"))
        assert get_input_param(params, "sitecode") == "DEABC"

    def test_returns_none_for_missing_ident(self):
        params = _params(("year", 2023))
        assert get_input_param(params, "missing") is None

    def test_returns_none_for_empty_params(self):
        assert get_input_param({"parameters": []}, "year") is None

    def test_returns_first_match(self):
        # If the same ident appears twice, the first value is returned.
        params = {"parameters": [
            {"ident": "x", "value": 1},
            {"ident": "x", "value": 2},
        ]}
        assert get_input_param(params, "x") == 1


class TestSetInputParam:
    def test_updates_existing_value(self):
        params = _params(("year", 2020))
        set_input_param(params, "year", 2024)
        assert get_input_param(params, "year") == 2024

    def test_does_not_affect_other_params(self):
        params = _params(("year", 2020), ("month", 6))
        set_input_param(params, "year", 2025)
        assert get_input_param(params, "month") == 6

    def test_set_to_none(self):
        params = _params(("year", 2020))
        set_input_param(params, "year", None)
        assert get_input_param(params, "year") is None

    def test_no_op_for_missing_ident(self):
        """set_input_param silently does nothing when ident is not found."""
        params = _params(("year", 2020))
        set_input_param(params, "unknown", 999)
        assert get_input_param(params, "year") == 2020


# ---------------------------------------------------------------------------
# yearmonth_parse
# ---------------------------------------------------------------------------

class TestYearmonthParse:
    def test_returns_tuple_of_five(self):
        result = yearmonth_parse("20230101-20230131")
        assert len(result) == 5

    def test_parses_start_and_end_dates(self):
        ym, start, end, start_str, end_str = yearmonth_parse("20230601-20230630")
        assert start == datetime(2023, 6, 1)
        assert end == datetime(2023, 6, 30)

    def test_returns_original_string_unchanged(self):
        period = "20230101-20231231"
        result = yearmonth_parse(period)
        assert result[0] == period

    def test_basename_strings_are_formatted(self):
        _, _, _, start_bn, end_bn = yearmonth_parse("20220315-20220430")
        assert start_bn == "20220315"
        assert end_bn == "20220430"

    def test_handles_whitespace_around_dash(self):
        _, start, end, _, _ = yearmonth_parse("20230101 - 20230131")
        assert start == datetime(2023, 1, 1)
        assert end == datetime(2023, 1, 31)


# ---------------------------------------------------------------------------
# get_next_month
# ---------------------------------------------------------------------------

class TestGetNextMonth:
    def test_normal_month(self):
        assert get_next_month("20230601") == "202307"

    def test_december_wraps_to_january_next_year(self):
        assert get_next_month("20231201") == "202401"

    def test_january(self):
        assert get_next_month("20230101") == "202302"

    def test_november(self):
        assert get_next_month("20231101") == "202312"


# ---------------------------------------------------------------------------
# generate_timeperiod_monthly
# ---------------------------------------------------------------------------

class TestGenerateTimeperiodMonthly:
    def test_january(self):
        result = generate_timeperiod_monthly(2023, 1)
        assert result == "20230101-20230131"

    def test_february_non_leap(self):
        result = generate_timeperiod_monthly(2023, 2)
        assert result == "20230201-20230228"

    def test_february_leap_year(self):
        result = generate_timeperiod_monthly(2024, 2)
        assert result == "20240201-20240229"

    def test_december(self):
        result = generate_timeperiod_monthly(2023, 12)
        assert result == "20231201-20231231"

    def test_april_30_days(self):
        result = generate_timeperiod_monthly(2023, 4)
        assert result == "20230401-20230430"

    def test_string_inputs_accepted(self):
        result = generate_timeperiod_monthly("2023", "6")
        assert result == "20230601-20230630"


# ---------------------------------------------------------------------------
# generate_timeperiod_quaterly
# ---------------------------------------------------------------------------

class TestGenerateTimeperiodQuaterly:
    def test_q1(self):
        result = generate_timeperiod_quaterly(2023, 1)
        assert result == "20230101-20230331"

    def test_q2(self):
        result = generate_timeperiod_quaterly(2023, 4)
        assert result == "20230401-20230630"

    def test_q3(self):
        result = generate_timeperiod_quaterly(2023, 7)
        assert result == "20230701-20230930"

    def test_q4(self):
        result = generate_timeperiod_quaterly(2023, 10)
        assert result == "20231001-20231231"


# ---------------------------------------------------------------------------
# generate_yearmonth_monthly (same logic as generate_timeperiod_monthly)
# ---------------------------------------------------------------------------

class TestGenerateYearmonthMonthly:
    def test_matches_timeperiod_monthly(self):
        assert generate_yearmonth_monthly(2023, 5) == generate_timeperiod_monthly(2023, 5)

    def test_december(self):
        assert generate_yearmonth_monthly(2022, 12) == "20221201-20221231"


# ---------------------------------------------------------------------------
# generate_yearmonth_quaterly
# ---------------------------------------------------------------------------

class TestGenerateYearmonthQuaterly:
    def test_matches_timeperiod_quaterly(self):
        assert generate_yearmonth_quaterly(2023, 4) == generate_timeperiod_quaterly(2023, 4)

    def test_q1(self):
        assert generate_yearmonth_quaterly(2023, 1) == "20230101-20230331"


# ---------------------------------------------------------------------------
# generate_yearmonth_monthbin
# ---------------------------------------------------------------------------

class TestGenerateYearmonthMonthbin:
    def test_bin_of_1_same_as_monthly(self):
        assert generate_yearmonth_monthbin(2023, 6, 1) == "20230601-20230731"

    def test_bin_of_2(self):
        result = generate_yearmonth_monthbin(2023, 1, 2)
        assert result == "20230101-20230331"

    def test_bin_of_3_same_as_quaterly(self):
        assert generate_yearmonth_monthbin(2023, 1, 3) == generate_timeperiod_quaterly(2023, 1)


# ---------------------------------------------------------------------------
# month_range
# ---------------------------------------------------------------------------

class TestMonthRange:
    def test_january(self):
        start, end = month_range("202301")
        assert start == datetime(2023, 1, 1)
        assert end == datetime(2023, 2, 1)

    def test_december_rolls_over_to_next_year(self):
        start, end = month_range("202312")
        assert start == datetime(2023, 12, 1)
        assert end == datetime(2024, 1, 1)

    def test_june(self):
        start, end = month_range("202306")
        assert start == datetime(2023, 6, 1)
        assert end == datetime(2023, 7, 1)

    def test_returns_tuple_of_datetimes(self):
        result = month_range("202301")
        assert all(isinstance(d, datetime) for d in result)


# ---------------------------------------------------------------------------
# quarter_range
# ---------------------------------------------------------------------------

class TestQuarterRange:
    def test_q1(self):
        start, end = quarter_range("202301")
        assert start == datetime(2023, 1, 1)
        assert end == datetime(2023, 4, 1)

    def test_q2(self):
        start, end = quarter_range("202302")
        assert start == datetime(2023, 4, 1)
        assert end == datetime(2023, 7, 1)

    def test_q3(self):
        start, end = quarter_range("202303")
        assert start == datetime(2023, 7, 1)
        assert end == datetime(2023, 10, 1)

    def test_q4_rolls_over_to_next_year(self):
        start, end = quarter_range("202304")
        assert start == datetime(2023, 10, 1)
        assert end == datetime(2024, 1, 1)


# ---------------------------------------------------------------------------
# generate_interval_by_span
# ---------------------------------------------------------------------------

class TestGenerateIntervalBySpan:
    def test_span_1(self):
        interval, end_year, end_month = generate_interval_by_span(2023, 1, 1)
        assert interval == "20230101-20230131"
        assert end_year == 2023 and end_month == 1

    def test_span_3_from_january(self):
        interval, end_year, end_month = generate_interval_by_span(2023, 1, 3)
        assert interval == "20230101-20230331"
        assert end_year == 2023 and end_month == 3

    def test_span_rolls_over_year(self):
        interval, end_year, end_month = generate_interval_by_span(2023, 11, 3)
        assert interval == "20231101-20240131"
        assert end_year == 2024 and end_month == 1

    def test_span_12_full_year(self):
        interval, end_year, end_month = generate_interval_by_span(2023, 1, 12)
        assert interval == "20230101-20231231"
        assert end_year == 2023 and end_month == 12

    def test_returns_three_tuple(self):
        result = generate_interval_by_span(2023, 6, 2)
        assert len(result) == 3


# ---------------------------------------------------------------------------
# has_subset_in_string
# ---------------------------------------------------------------------------

class TestHasSubsetInString:
    def test_finds_substring(self):
        assert has_subset_in_string("sentinel2_tile_T32UQD.tif", ["sentinel2"]) is True

    def test_returns_false_when_not_found(self):
        assert has_subset_in_string("landsat8_scene.tif", ["sentinel2"]) is False

    def test_case_insensitive(self):
        assert has_subset_in_string("Sentinel2_tile.tif", ["sentinel2"]) is True
        assert has_subset_in_string("LANDSAT8.tif", ["landsat8"]) is True

    def test_matches_any_in_list(self):
        assert has_subset_in_string("landsat8_scene.tif", ["sentinel2", "landsat8"]) is True

    def test_empty_subset_list(self):
        assert has_subset_in_string("anything.tif", []) is False

    def test_empty_string(self):
        assert has_subset_in_string("", ["sentinel"]) is False


# ---------------------------------------------------------------------------
# parse_sentinel2_name
# ---------------------------------------------------------------------------

# Example: S2A_MSIL1C_20200615T103021_N0209_R108_T32UQD_20200615T130445
_S2_L1C = "S2A_MSIL1C_20200615T103021_N0209_R108_T32UQD_20200615T130445"
_S2_L2A = "S2B_MSIL2A_20220315T100549_N0400_R122_T33UXR_20220315T120000"
_S2_NEW_BASELINE = "S2A_MSIL1C_20230101T100000_N0500_R108_T32UQD_20230101T120000"

class TestParseSentinel2Name:
    def test_basic_fields(self):
        info = parse_sentinel2_name(_S2_L1C)
        assert info["mission"] == "A"
        assert info["level"] == "L1C"
        assert info["tile"] == "32UQD"
        assert info["rel_orbit"] == "108"
        assert info["satellite"] == "sentinel-2"

    def test_observation_date(self):
        info = parse_sentinel2_name(_S2_L1C)
        assert info["obs_date"] == datetime(2020, 6, 15, 10, 30, 21)

    def test_produced_date(self):
        info = parse_sentinel2_name(_S2_L1C)
        assert info["produced_date"] == datetime(2020, 6, 15, 13, 4, 45)

    def test_strips_dot_safe(self):
        info = parse_sentinel2_name(_S2_L1C + ".SAFE")
        assert info["name"] == _S2_L1C

    def test_old_baseline_no_new_radiometric_correction(self):
        info = parse_sentinel2_name(_S2_L1C)  # baseline N0209 < 400
        assert info["radiometric_correction"] is False

    def test_new_baseline_number_triggers_correction(self):
        # N0400 → baseline 400 >= 400 → new radiometric correction
        info = parse_sentinel2_name(_S2_L2A)
        assert info["radiometric_correction"] is True

    def test_date_after_2022_01_26_always_new_correction(self):
        info = parse_sentinel2_name(_S2_NEW_BASELINE)
        assert info["radiometric_correction"] is True

    def test_name_field_preserved(self):
        info = parse_sentinel2_name(_S2_L1C)
        assert info["name"] == _S2_L1C


# ---------------------------------------------------------------------------
# compile_sentinel2level2a_glob
# ---------------------------------------------------------------------------

class TestCompileSentinel2Level2aGlob:
    def test_returns_path_and_glob(self):
        path, glob = compile_sentinel2level2a_glob(_S2_L1C)
        assert isinstance(path, Path)
        assert isinstance(glob, str)

    def test_path_structure(self):
        path, _ = compile_sentinel2level2a_glob(_S2_L1C)
        parts = path.parts
        assert parts[0] == "Sentinel2"
        assert parts[1] == "2020"   # year
        assert parts[2] == "06"     # month
        assert parts[3] == "15"     # day

    def test_glob_contains_l2a_and_tile(self):
        _, glob = compile_sentinel2level2a_glob(_S2_L1C)
        assert "MSIL2A" in glob
        assert "T32UQD" in glob
        assert glob.endswith(".SAFE")

    def test_glob_uses_same_mission(self):
        _, glob = compile_sentinel2level2a_glob(_S2_L1C)
        assert glob.startswith("S2A")


# ---------------------------------------------------------------------------
# return_tilename / return_eodata_tilename
# ---------------------------------------------------------------------------

class TestReturnTilename:
    def test_strips_suffix_after_underscore(self):
        assert return_tilename("T32UQD_32632") == "T32UQD"

    def test_returns_unchanged_when_no_underscore(self):
        assert return_tilename("T32UQD") == "T32UQD"

    def test_strips_only_first_underscore_part(self):
        assert return_tilename("T32UQD_A_B") == "T32UQD"


class TestReturnEodataTilename:
    def test_strips_suffix_after_dash(self):
        assert return_eodata_tilename("T32UQD-sub") == "T32UQD"

    def test_returns_unchanged_when_no_dash(self):
        assert return_eodata_tilename("T32UQD") == "T32UQD"
