import pytest

from app.services.catalog.csv_source import CsvFormatError, parse_products

HEADER = ("Handle,Title,Body (HTML),Vendor,Type,Tags,Published,"
          "Option1 Name,Option1 Value,Option2 Name,Option2 Value,"
          "Variant SKU,Variant Inventory Qty,Variant Price,"
          "Variant Compare At Price,Image Src,Status")

SHIRT = ("oxford-shirt,Oxford Shirt,A white shirt.,Brooks,Shirts,\"formal,cotton\","
         "TRUE,Size,M,Colour,White,SKU-M,12,59.00,79.00,"
         "https://x.example/s.jpg,active")
SHIRT_L = ("oxford-shirt,Oxford Shirt,A white shirt.,Brooks,Shirts,\"formal,cotton\","
           "TRUE,Size,L,Colour,White,SKU-L,0,65.00,79.00,"
           "https://x.example/s.jpg,active")


def parse(*rows, currency="USD"):
    text = "\n".join((HEADER,) + rows)
    return parse_products(text, "test.csv", currency)


def test_one_row_is_one_product():
    products, errors = parse(SHIRT)
    assert errors == []
    assert len(products) == 1
    assert products[0]["title"] == "Oxford Shirt"
    assert products[0]["brand"] == "Brooks"
    assert products[0]["product_type"] == "Shirts"


def test_rows_sharing_a_handle_are_one_product():
    products, _ = parse(SHIRT, SHIRT_L)
    assert len(products) == 1
    assert len(products[0]["variants"]) == 2


def test_the_lowest_variant_price_wins():
    # Matches how the Shopify connector already treats variants.
    products, _ = parse(SHIRT, SHIRT_L)
    assert min(v["price"] for v in products[0]["variants"]) == 59.00


def test_a_product_is_in_stock_if_any_variant_is():
    products, _ = parse(SHIRT, SHIRT_L)
    assert any(v["available"] for v in products[0]["variants"])


def test_a_product_is_out_of_stock_when_every_variant_is():
    zero = SHIRT.replace(",12,", ",0,")
    products, _ = parse(zero)
    assert not any(v["available"] for v in products[0]["variants"])


def test_options_are_collected():
    products, _ = parse(SHIRT)
    assert products[0]["variants"][0]["options"] == {"Size": "M", "Colour": "White"}


def test_a_single_option_product_has_no_empty_second_option():
    row = ("belt,Leather Belt,A belt.,Brooks,Belts,leather,TRUE,"
           "Size,M,,,SKU-B,5,29.00,,https://x.example/b.jpg,active")
    products, _ = parse(row)
    assert products[0]["variants"][0]["options"] == {"Size": "M"}


def test_tags_become_a_list():
    products, _ = parse(SHIRT)
    assert products[0]["tags"] == ["formal", "cotton"]


def test_product_fields_come_from_the_first_row_of_a_handle():
    # A real Shopify export leaves them blank on later rows.
    blank = "oxford-shirt,,,,,,,Size,L,Colour,Blue,SKU-B,4,59.00,,,active"
    products, errors = parse(SHIRT, blank)
    assert errors == []
    assert products[0]["title"] == "Oxford Shirt"
    assert products[0]["brand"] == "Brooks"
    assert len(products[0]["variants"]) == 2


def test_every_product_carries_the_declared_currency():
    products, _ = parse(SHIRT, currency="INR")
    assert products[0]["currency"] == "INR"


def test_the_source_is_recorded():
    products, _ = parse(SHIRT)
    assert products[0]["source_kind"] == "csv"
    assert products[0]["source_ref"] == "test.csv"


def test_the_handle_is_the_external_id():
    products, _ = parse(SHIRT)
    assert products[0]["external_id"] == "oxford-shirt"


# --- row-level errors, all survivable ---------------------------------------

def test_a_row_with_no_handle_is_skipped_and_reported():
    bad = ",No Handle,desc,Brooks,Shirts,,TRUE,Size,M,,,SKU,1,9.00,,,active"
    products, errors = parse(SHIRT, bad)
    assert len(products) == 1
    assert errors[0]["row"] == 3
    assert "Handle" in errors[0]["reason"]


def test_a_first_row_with_no_title_is_skipped_and_reported():
    bad = "ghost,,desc,Brooks,Shirts,,TRUE,Size,M,,,SKU,1,9.00,,,active"
    products, errors = parse(bad)
    assert products == []
    assert errors[0]["row"] == 2
    assert "Title" in errors[0]["reason"]


def test_an_unparseable_price_is_skipped_and_reported():
    bad = SHIRT.replace(",59.00,", ",ask,")
    products, errors = parse(bad)
    assert products == []
    assert errors[0]["row"] == 2
    assert "Price" in errors[0]["reason"]


def test_a_negative_price_is_rejected():
    bad = SHIRT.replace(",59.00,", ",-5.00,")
    _, errors = parse(bad)
    assert errors and errors[0]["row"] == 2


def test_a_negative_quantity_is_rejected():
    bad = SHIRT.replace(",12,", ",-3,")
    _, errors = parse(bad)
    assert errors and errors[0]["row"] == 2


def test_row_numbers_match_the_spreadsheet():
    # Header is row 1, so the first data row is row 2. An off-by-one here sends
    # a merchant to the wrong line in their file.
    bad = ",No Handle,d,B,T,,TRUE,Size,M,,,S,1,9.00,,,active"
    _, errors = parse(SHIRT, SHIRT_L, bad)
    assert errors[0]["row"] == 4


def test_a_file_where_every_row_fails_is_not_an_exception():
    bad = ",,,,,,,,,,,,,,,,"
    products, errors = parse(bad)
    assert products == []
    assert errors


# --- file-level errors, fatal ------------------------------------------------

def test_a_missing_required_column_is_fatal():
    with pytest.raises(CsvFormatError):
        parse_products("Title,Vendor\nA,B", "x.csv", "USD")


def test_a_file_with_no_data_rows_is_fatal():
    with pytest.raises(CsvFormatError):
        parse_products(HEADER, "x.csv", "USD")


def test_empty_input_is_fatal():
    with pytest.raises(CsvFormatError):
        parse_products("", "x.csv", "USD")


def test_a_missing_currency_is_accepted():
    # Optional, exactly as it is when connecting a product API. Never guessed:
    # the products simply carry no currency and are flagged, which affects
    # comparing prices across sources and nothing else.
    products, errors = parse_products(f"{HEADER}\n{SHIRT}", "x.csv", "")
    assert errors == []
    assert products[0]["currency"] is None


def test_unknown_columns_are_ignored():
    # A real Shopify export has ~44 columns; we read 17. Rejecting a file for
    # carrying "Variant Weight Unit" would break "export and upload".
    header = HEADER + ",Variant Weight Unit,Gift Card"
    row = SHIRT + ",g,FALSE"
    products, errors = parse_products(f"{header}\n{row}", "x.csv", "USD")
    assert errors == []
    assert len(products) == 1


# --- friendly header aliases -------------------------------------------------

FRIENDLY_HEADER = ("Handle,Title,Description,Brand,Category,Tags,Published,"
                   "Option1 Name,Option1 Value,Option2 Name,Option2 Value,"
                   "SKU,Quantity,Price,Compare At Price,Image URL,Status")

FRIENDLY_ROW = ("oxford-shirt,Oxford Shirt,A white shirt.,Brooks,Shirts,"
                "\"formal,cotton\",TRUE,Size,M,Colour,White,SKU-M,12,59.00,"
                "79.00,https://x.example/s.jpg,active")


def test_friendly_headers_parse_the_same_as_shopify_headers():
    # "Body (HTML)" is a historical Shopify artefact and reads as a demand for
    # HTML to anyone filling the template in by hand.
    friendly, errors = parse_products(
        f"{FRIENDLY_HEADER}\n{FRIENDLY_ROW}", "test.csv", "USD")
    shopify, _ = parse(SHIRT)
    assert errors == []
    assert friendly[0]["description"] == shopify[0]["description"]
    assert friendly[0]["brand"] == shopify[0]["brand"]
    assert friendly[0]["product_type"] == shopify[0]["product_type"]
    assert friendly[0]["image_url"] == shopify[0]["image_url"]
    assert friendly[0]["variants"][0]["price"] == shopify[0]["variants"][0]["price"]


def test_aliases_are_case_insensitive():
    header = FRIENDLY_HEADER.replace("Description", "description")
    products, errors = parse_products(f"{header}\n{FRIENDLY_ROW}", "x.csv", "USD")
    assert errors == []
    assert products[0]["description"] == "A white shirt."


def test_the_two_spellings_can_be_mixed_in_one_file():
    header = FRIENDLY_HEADER.replace("Price", "Variant Price")
    products, errors = parse_products(f"{header}\n{FRIENDLY_ROW}", "x.csv", "USD")
    assert errors == []
    assert products[0]["variants"][0]["price"] == 59.00


def test_a_product_url_column_is_read():
    # Without a URL the product cannot be linked to, and normalise() flags it
    # as incomplete, which keeps it out of pairing and out of recommendations.
    header = FRIENDLY_HEADER + ",Product URL"
    row = FRIENDLY_ROW + ",https://shop.example/products/oxford-shirt"
    products, errors = parse_products(f"{header}\n{row}", "x.csv", "USD")
    assert errors == []
    assert products[0]["product_url"] == "https://shop.example/products/oxford-shirt"


def test_a_missing_product_url_column_is_still_accepted():
    products, _ = parse(SHIRT)
    assert products[0]["product_url"] is None


# --- the Currency column -----------------------------------------------------

CURRENCY_HEADER = FRIENDLY_HEADER.replace("Price,", "Price,Currency,", 1)


def _with_currency(row_currency):
    row = FRIENDLY_ROW.replace("59.00,", f"59.00,{row_currency},", 1)
    return f"{CURRENCY_HEADER}\n{row}"


def test_a_currency_column_is_read_per_product():
    products, errors = parse_products(_with_currency("GBP"), "x.csv")
    assert errors == []
    assert products[0]["currency"] == "GBP"


def test_the_column_wins_over_the_upload_wide_value():
    # The row is the more specific statement, and it lets one file hold two
    # currencies.
    products, _ = parse_products(_with_currency("GBP"), "x.csv", "USD")
    assert products[0]["currency"] == "GBP"


def test_a_blank_cell_falls_back_to_the_upload_value():
    products, _ = parse_products(_with_currency(""), "x.csv", "USD")
    assert products[0]["currency"] == "USD"


def test_currency_is_upper_cased():
    products, _ = parse_products(_with_currency("gbp"), "x.csv")
    assert products[0]["currency"] == "GBP"


def test_one_file_can_hold_two_currencies():
    rows = [CURRENCY_HEADER,
            FRIENDLY_ROW.replace("59.00,", "59.00,GBP,", 1),
            FRIENDLY_ROW.replace("oxford-shirt", "navy-chinos")
                        .replace("59.00,", "69.00,EUR,", 1)]
    products, errors = parse_products("\n".join(rows), "x.csv")
    assert errors == []
    assert {p["currency"] for p in products} == {"GBP", "EUR"}

