# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

import os
import sys
import tempfile
import time
import unicodedata
from typing import get_args

import pytest

from hypothesis import given, strategies as st
from hypothesis.errors import InvalidArgument
from hypothesis.internal import charmap as cm
from hypothesis.internal.intervalsets import IntervalSet

from tests.common.utils import skipif_threading


def test_charmap_contains_all_unicode():
    n = 0
    for vs in cm.charmap().values():
        for u, v in vs:
            n += v - u + 1
    assert n == sys.maxunicode + 1


def test_charmap_has_right_categories():
    for cat, intervals in cm.charmap().items():
        for u, v in intervals:
            for i in range(u, v + 1):
                real = unicodedata.category(chr(i))
                assert real == cat, f"{i} is {real} but reported in {cat}"


def assert_valid_range_list(ls):
    for u, v in ls:
        assert u <= v
    for i in range(len(ls) - 1):
        assert ls[i] <= ls[i + 1]
        assert ls[i][-1] < ls[i + 1][0]


@given(st.sets(st.sampled_from(cm.categories())))
def test_query_matches_categories(cats):
    values = cm.query(categories=cats).intervals
    assert_valid_range_list(values)
    for u, v in values:
        for i in (u, v, (u + v) // 2):
            assert unicodedata.category(chr(i)) in cats


@given(
    st.sets(st.sampled_from(cm.categories())) | st.none(),
    st.integers(0, sys.maxunicode),
    st.integers(0, sys.maxunicode),
)
def test_query_matches_categories_codepoints(cats, m1, m2):
    m1, m2 = sorted((m1, m2))
    values = cm.query(categories=cats, min_codepoint=m1, max_codepoint=m2).intervals
    assert_valid_range_list(values)
    for u, v in values:
        assert m1 <= u
        assert v <= m2


# any test which sets `_charmap = None` is thread-unsafe.
@skipif_threading
def test_reload_charmap():
    x = cm.charmap()
    assert x is cm.charmap()
    cm._charmap = None
    y = cm.charmap()
    assert x is not y
    assert x == y


@skipif_threading
def test_recreate_charmap():
    x = cm.charmap()
    assert x is cm.charmap()
    cm._charmap = None
    cm.charmap_file().unlink()
    y = cm.charmap()
    assert x is not y
    assert x == y


# This test fails flakily (every 1 in ~10 full CI runs), but only on the test-pyodide
# ci job:
#
#      os.utime(cm.charmap_file(), (mtime, mtime))
#      ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#   FileNotFoundError: [Errno 44] No such file or directory:
#   '/home/runner/work/hypothesis/hypothesis/.hypothesis/unicode_data/15.1.0/charmap.json.gz'
#
# I suspect this is from a race condition due to how we parallelize the pyodide
# tests in CI, (splitting test files across 20 processes). It's also possible
# it's from some pyodide-specific weirdness, but since the test only fails sometimes,
# I find this less likely.
#
# I'm xfailing this on emscripten for now, to get a consistent green CI.
@pytest.mark.xfail(
    condition=sys.platform == "emscripten", strict=False, reason="see comment"
)
@skipif_threading
def test_uses_cached_charmap():
    cm.charmap()

    # Reset the last-modified time of the cache file to a point in the past.
    mtime = int(time.time() - 1000)
    os.utime(cm.charmap_file(), (mtime, mtime))
    statinfo = cm.charmap_file().stat()
    assert statinfo.st_mtime == mtime

    # Force reload of charmap from cache file and check that mtime is unchanged.
    cm._charmap = None
    cm.charmap()
    statinfo = cm.charmap_file().stat()
    assert statinfo.st_mtime == mtime


def _union_intervals(x, y):
    return IntervalSet(x).union(IntervalSet(y)).intervals


def test_union_empty():
    assert _union_intervals([], []) == ()
    assert _union_intervals([], [[1, 2]]) == ((1, 2),)
    assert _union_intervals([[1, 2]], []) == ((1, 2),)


def test_union_handles_totally_overlapped_gap():
    #   < xx  >  Imagine the intervals x and y as bit strings.
    # | <yy yy>  The bit at position n is set if n falls inside that interval.
    # = <zzzzz>  In this model _union_intervals() performs bit-wise or.
    assert _union_intervals([[2, 3]], [[1, 2], [4, 5]]) == ((1, 5),)


def test_union_handles_partially_overlapped_gap():
    #   <  x  >  Imagine the intervals x and y as bit strings.
    # | <yy  y>  The bit at position n is set if n falls inside that interval.
    # = <zzz z>  In this model _union_intervals() performs bit-wise or.
    assert _union_intervals([[3, 3]], [[1, 2], [5, 5]]) == ((1, 3), (5, 5))


def test_successive_union():
    x = []
    for v in cm.charmap().values():
        x = _union_intervals(x, v)
    assert x == ((0, sys.maxunicode),)


@skipif_threading
def test_can_handle_race_between_exist_and_create(monkeypatch):
    x = cm.charmap()
    cm._charmap = None
    monkeypatch.setattr(os.path, "exists", lambda p: False)
    y = cm.charmap()
    assert x is not y
    assert x == y


@skipif_threading
def test_exception_in_write_does_not_lead_to_broken_charmap(monkeypatch):
    def broken(*args, **kwargs):
        raise ValueError

    cm._charmap = None
    monkeypatch.setattr(os.path, "exists", lambda p: False)
    monkeypatch.setattr(os, "rename", broken)

    cm.charmap()
    cm.charmap()


@skipif_threading
def test_regenerate_broken_charmap_file():
    cm.charmap()

    cm.charmap_file().write_bytes(b"")  # overwrite with empty file

    cm._charmap = None
    cm.charmap()


def test_query_rejects_min_codepoint_greater_than_max():
    with pytest.raises(InvalidArgument):
        cm.query(min_codepoint=1, max_codepoint=0)


def test_exclude_characters_are_included_in_key():
    assert cm.query().intervals != cm.query(exclude_characters="0").intervals


@skipif_threading
def test_error_writing_charmap_file_is_suppressed(monkeypatch):
    def broken_mkstemp(dir):
        raise RuntimeError

    monkeypatch.setattr(tempfile, "mkstemp", broken_mkstemp)

    try:
        # Cache the charmap to avoid a performance hit the next time
        # somebody tries to use it.
        saved = cm._charmap
        cm._charmap = None
        cm.charmap_file().unlink()

        cm.charmap()
    finally:
        cm._charmap = saved


def test_categoryname_literal_is_correct():
    minor_categories = set(cm.categories())
    major_categories = {c[0] for c in minor_categories}
    assert set(get_args(cm.CategoryName)) == minor_categories | major_categories
