"""
El gráfico del inicio y la tabla que lo acompaña salen de la misma llamada.

Es lo único que garantiza que las diez líneas y las diez filas sean **las mismas
diez y en el mismo orden**. Con dos consultas separadas, un empate en impresiones
las podría dejar mostrando conjuntos distintos, y eso no se ve roto: se ven dos
listas de diez, y la leyenda del gráfico nombra keywords que la tabla de abajo no
tiene.

Lo otro que se defiende acá es el eje del tiempo. El gráfico dibuja un hueco
donde Google no reportó, y el hueco no lo manda el servidor —mandar doscientos
ochenta nulos para no decir nada es peso puro—: lo arma la pantalla cruzando los
puntos contra `dates`. Si `dates` no cubriera la ventana entera, los días que
faltan desaparecerían del eje en vez de dibujarse vacíos, y la serie se vería
continua.
"""

from datetime import timedelta

import pytest

from apps.seo import services
from apps.seo.models import KeywordDaily, SyncState
from tests.factories import create_account, create_domain


@pytest.fixture
def domain(db):
    return create_domain(create_account())


@pytest.fixture
def site(domain):
    SyncState.objects.create(domain=domain, last_closed_date=services.last_available_date())
    return domain


def record(domain, query, *, days_ago=0, impressions=100, clicks=5, position=4.0):
    return KeywordDaily.objects.create(
        domain=domain,
        date=services.last_available_date() - timedelta(days=days_ago),
        query=query,
        clicks=clicks,
        impressions=impressions,
        ctr=clicks / impressions if impressions else 0.0,
        position=position,
    )


class TestWithoutHistory:
    def test_a_site_with_no_history_draws_nothing(self, domain):
        """
        `None` y no una estructura vacía: un gráfico sin una sola línea se lee
        como un error de carga, y lo que corresponde es no dibujarlo.
        """
        SyncState.objects.create(domain=domain)

        assert services.top_keywords_series(domain) is None

    def test_a_site_with_a_date_but_no_rows_draws_nothing(self, site):
        assert services.top_keywords_series(site) is None


class TestTheChartAndTheTableMatch:
    def test_both_describe_the_same_keywords_in_the_same_order(self, site):
        for index in range(4):
            record(site, f'keyword-{index}', impressions=(index + 1) * 10)

        data = services.top_keywords_series(site)

        assert [row['query'] for row in data['keywords']] == [
            entry['query'] for entry in data['series']
        ]

    def test_they_come_ordered_by_impressions(self, site):
        record(site, 'poca', impressions=10)
        record(site, 'mucha', impressions=900)
        record(site, 'media', impressions=100)

        data = services.top_keywords_series(site)

        assert [row['query'] for row in data['keywords']] == ['mucha', 'media', 'poca']

    def test_only_the_top_ones_travel(self, site):
        for index in range(15):
            record(site, f'keyword-{index:02d}', impressions=index + 1)

        data = services.top_keywords_series(site, limit=10)

        assert len(data['keywords']) == 10
        assert len(data['series']) == 10
        # Las diez con más impresiones, no las diez primeras alfabéticamente.
        assert data['keywords'][0]['query'] == 'keyword-14'

    def test_the_table_rows_have_the_shape_of_the_keywords_table(self, site):
        """
        La tabla del pie es la de la vista de keywords con otro conjunto, no otra
        tabla: si las filas cambiaran de forma, habría que declarar sus columnas
        dos veces y se separarían en la primera que alguien tocara.
        """
        record(site, 'una')

        row = services.top_keywords_series(site)['keywords'][0]

        assert set(row) == {
            'query',
            'position',
            'impressions',
            'clicks',
            'ctr',
            'deltas',
            'tracked',
            'band',
            'engagement',
        }


class TestTheTimeAxis:
    def test_the_axis_covers_the_whole_window(self, site):
        record(site, 'una')

        data = services.top_keywords_series(site, days=28)

        assert len(data['dates']) == 28
        assert data['dates'][-1] == services.last_available_date().isoformat()

    def test_a_day_google_did_not_report_is_absent_from_the_points(self, site):
        """
        El hueco lo arma la pantalla cruzando contra `dates`.

        Mandar un nulo por cada día sin dato serían doscientos ochenta nulos para
        no decir nada, y con diez keywords de cola larga la mayoría de los días
        están vacíos.
        """
        record(site, 'una', days_ago=0)
        record(site, 'una', days_ago=2)

        data = services.top_keywords_series(site)
        points = data['series'][0]['points']

        assert len(points) == 2
        assert len(data['dates']) > len(points)

    def test_the_points_come_in_order(self, site):
        for day in (5, 1, 3):
            record(site, 'una', days_ago=day)

        points = services.top_keywords_series(site)['series'][0]['points']

        assert [point['date'] for point in points] == sorted(point['date'] for point in points)

    def test_every_point_carries_the_three_metrics(self, site):
        """
        Las tres viajan juntas para que cambiar de métrica no vaya a la red, que
        es lo único que justifica que sea un interruptor y no un filtro.
        """
        record(site, 'una')

        point = services.top_keywords_series(site)['series'][0]['points'][0]

        assert set(point) == {'date', 'impressions', 'clicks', 'position'}
