from __future__ import annotations

from concurrent.futures import ProcessPoolExecutor
from datetime import datetime, timedelta, timezone
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any
from zoneinfo import ZoneInfo

# NOTE: pytest ... -s --hypothesis-verbosity=debug is useful for seeing what hypothesis is doing
import pytest
from hypothesis import given, settings
from hypothesis.strategies import from_type

from ..common import Loc
from ..database.common import DbVisit
from ..database.dump import visits_to_sqlite
from ..database.load import get_all_db_visits
from ..sqlite import sqlite_connection
from .common import (
    gc_control,  # noqa: F401
    running_on_ci,
)

HSETTINGS: dict[str, Any] = {
    'derandomize': True,
    'deadline': timedelta(seconds=2),  # sometimes slow on ci
}


def test_no_visits(tmp_path: Path) -> None:
    visits: list[DbVisit] = []

    db = tmp_path / 'db.sqlite'
    errors = visits_to_sqlite(
        vit=visits,
        overwrite_db=True,
        _db_path=db,
    )
    assert db.exists()
    [err] = [errors]
    assert 'No visits were indexed' in str(err)


def test_one_visit(tmp_path: Path) -> None:
    dt = datetime.fromisoformat('2023-11-14T23:11:01')
    dt = dt.replace(tzinfo=ZoneInfo('Europe/Warsaw'))
    visit = DbVisit(
        norm_url='google.com',
        orig_url='https://google.com',
        dt=dt,
        locator=Loc.make(title='title', href='https://whatever.com'),
        duration=123,
        src='whatever',
    )

    visits = [visit]

    db = tmp_path / 'db.sqlite'
    errors = visits_to_sqlite(
        vit=visits,
        overwrite_db=True,
        _db_path=db,
    )
    assert len(errors) == 0
    assert db.exists()

    with sqlite_connection(db, row_factory='dict') as conn:
        [sqlite_visit] = conn.execute('SELECT * FROM visits')

    assert sqlite_visit == {
        'context': None,
        'dt': '2023-11-14T23:11:01+01:00',
        'duration': 123,
        'locator_href': 'https://whatever.com',
        'locator_title': 'title',
        'norm_url': 'google.com',
        'orig_url': 'https://google.com',
        'src': 'whatever',
    }

    visits_in_db = get_all_db_visits(db)
    assert visits_in_db == [visit]


def test_read_db_visits(tmp_path: Path) -> None:
    """
    Deliberately test against "hardcoded" database to check for backwards compatibility
    """
    db = tmp_path / 'db.sqlite'
    with sqlite_connection(db) as conn:
        conn.execute(
            '''
CREATE TABLE visits (
    norm_url VARCHAR,
    orig_url VARCHAR,
    dt VARCHAR,
    locator_title VARCHAR,
    locator_href VARCHAR,
    src VARCHAR,
    context VARCHAR,
    duration INTEGER
);
'''
        )
        # this dt format (zone name after iso timestap) might occur in legacy databases
        # (that were created when promnesia was using cachew NTBinder)
        conn.execute(
            '''
INSERT INTO visits VALUES(
    'i.redd.it/alala.jpg',
    'https://i.redd.it/alala.jpg',
    '2019-04-13T11:55:09-04:00 America/New_York',
    'Reddit save',
    'https://reddit.com/r/whatever',
    'reddit',
    '',
    NULL
);
'''
        )
    [visit_in_db] = get_all_db_visits(db)
    assert visit_in_db == DbVisit(
        norm_url='i.redd.it/alala.jpg',
        orig_url='https://i.redd.it/alala.jpg',
        dt=datetime(2019, 4, 13, 11, 55, 9, tzinfo=timezone(timedelta(hours=-4))),
        locator=Loc.make(title='Reddit save', href='https://reddit.com/r/whatever'),
        src='reddit',
        context='',
    )


def _test_random_visit_aux(visit: DbVisit, tmp_path: Path) -> None:
    db = tmp_path / 'db.sqlite'
    errors = visits_to_sqlite(
        vit=[visit],
        overwrite_db=True,
        _db_path=db,
    )
    assert db.exists()
    assert len(errors) == 0, errors
    # TODO query the db?


@given(
    visit=from_type(DbVisit).filter(
        # if duration is too big it fails to insert in sqlite
        lambda v: (v.duration is None or 0 <= v.duration <= 10**5)
    )
)
@settings(**HSETTINGS, max_examples=100)
def test_random_visit(visit: DbVisit) -> None:
    with TemporaryDirectory() as tdir:
        tmp_path = Path(tdir)
        _test_random_visit_aux(visit=visit, tmp_path=tmp_path)


_dt_naive = datetime.fromisoformat('2023-11-14T23:11:01')
_dt_aware = _dt_naive.replace(tzinfo=ZoneInfo('America/New_York'))


def make_testvisit(i: int) -> DbVisit:
    return DbVisit(
        norm_url=f'google.com/{i}',
        orig_url=f'https://google.com/{i}',
        dt=(_dt_naive if i % 2 == 0 else _dt_aware) + timedelta(seconds=i),
        locator=Loc.make(title=f'title{i}', href=f'https://whatever.com/{i}'),
        duration=i,
        src='whatever',
    )


@pytest.mark.parametrize('count', [99, 100_000, 1_000_000])
@pytest.mark.parametrize('gc_on', [True, False], ids=['gc_on', 'gc_off'])
def test_benchmark_visits_dumping(count: int, gc_control, tmp_path: Path) -> None:
    # [20231212] testing differernt CHUNK_BY values with 1_000_000 visits on @karlicoss desktop pc
    # 1: 25s (perhaps most overhead is from temporary lists?)
    # 10 (current default): 8s
    # 100: 6s
    # 1000: 6s
    # TODO maybe consider changing default to 100?
    if count > 99 and running_on_ci:
        pytest.skip("test would be too slow on CI, only meant to run manually")

    visits = (make_testvisit(i) for i in range(count))
    db = tmp_path / 'db.sqlite'
    errors = visits_to_sqlite(  # TODO maybe this method should return db stats? would make testing easier
        vit=visits,
        overwrite_db=True,
        _db_path=db,
    )
    assert db.exists()
    assert len(errors) == 0, errors


def _populate_db(db_path: Path, *, overwrite_db: bool, count: int) -> None:
    visits = [make_testvisit(i) for i in range(count)]
    errors = visits_to_sqlite(visits, _db_path=db_path, overwrite_db=overwrite_db)
    assert len(errors) == 0


@pytest.mark.parametrize('mode', ['update', 'overwrite'])
def test_concurrent(tmp_path: Path, mode: str) -> None:
    overwrite_db = {'overwrite': True, 'update': False}[mode]

    db_path = tmp_path / 'db.sqlite'
    # do initial indexing to initialize the db
    _populate_db(db_path, overwrite_db=True, count=1)
    assert db_path.exists()  # just in case

    # this simply tests correctness by running many concurrent indexers
    parallel = 100  # 100 indexers
    with ProcessPoolExecutor(max_workers=8) as pool:
        futures = []
        for _ in range(parallel):
            futures.append(pool.submit(_populate_db, db_path, overwrite_db=overwrite_db, count=1_000))
        for f in futures:
            f.result()
    assert db_path.exists()  # just in case


# TODO test to make sure db is readable while we're indexing?
# kinda nicer version of test_query_while_indexing
