#  pylint: disable=missing-module-docstring
import logging
import pytest
import psycopg2
import shlex
import subprocess
from mgr_events import Responder, DEFAULT_COMMIT_BURST
from unittest.mock import MagicMock, patch, call
from sqlalchemy import create_engine
from sqlalchemy_utils import database_exists, create_database, drop_database

ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
log = logging.getLogger("mgr_events")
log.setLevel(logging.DEBUG)
log.addHandler(ch)


@pytest.fixture(scope="session")
def postgres(request):
    proc = subprocess.Popen(
        shlex.split('su postgres -c "pg_ctl -D ~/data -l ~/logfile start"')
    )

    def finalizer():
        subprocess.Popen(
            shlex.split('su postgres -c "pg_ctl -D /var/lib/pgsql/data stop"')
        )

    request.addfinalizer(finalizer)
    # pylint: disable-next=unused-variable
    outs, errs = proc.communicate(timeout=15)
    yield proc


@pytest.fixture(scope="session")
# pylint: disable-next=redefined-outer-name,unused-argument
def db_engine(postgres):
    return create_engine("postgresql://postgres@/test")


@pytest.fixture
# pylint: disable-next=redefined-outer-name
def db_connection(db_engine):
    if not database_exists(db_engine.url):
        create_database(db_engine.url)
    with psycopg2.connect(
        user="postgres", host="localhost", dbname="test"
    ) as connection:
        yield connection
    drop_database(db_engine.url)


def new_connection():
    return psycopg2.connect(user="postgres", host="localhost", dbname="test")


@pytest.fixture
# pylint: disable-next=redefined-outer-name
def create_tables(db_connection):
    sql = """CREATE TABLE suseSaltEvent (
        id SERIAL PRIMARY KEY,
        minion_id CHARACTER VARYING(256),
        data TEXT NOT NULL,
        queue NUMERIC NOT NULL
    );"""
    db_connection.cursor().execute(sql)
    db_connection.commit()


def delete_table(conn, table):
    # pylint: disable-next=consider-using-f-string
    conn.cursor().execute("DELETE FROM %s" % table)
    conn.commit()


@pytest.fixture
# pylint: disable-next=redefined-outer-name,unused-argument
def responder(db_connection, create_tables):
    with patch("mgr_events.psycopg2") as mock_psycopg2:
        mock_psycopg2.connect.return_value = db_connection
        return Responder(
            MagicMock(),  # mock event_bus
            {
                "postgres_db": {
                    "dbname": "tests",
                    "user": "postgres",
                    "password": "",
                    "host": "localhost",
                    "notify_channel": "suseSaltEvent",
                },
                "events": {"thread_pool_size": 3},
            },
        )


# pylint: disable-next=redefined-outer-name
def test_connection_recovery_on_insert(db_connection, responder):
    disposable_connection = new_connection()
    responder.connection = disposable_connection
    # pylint: disable-next=protected-access
    responder._insert("salt/minion/1/start", {"value": 1})
    responder.connection.close()
    with patch("mgr_events.psycopg2") as mock_psycopg2:
        mock_psycopg2.connect.return_value = db_connection
        # pylint: disable-next=protected-access
        responder._insert("salt/minion/2/start", {"value": 2})
    responder.connection.commit()
    responder.cursor.execute("SELECT * FROM suseSaltEvent")
    resp = responder.cursor.fetchall()
    assert len(resp) == 2


# pylint: disable-next=redefined-outer-name
def test_connection_recovery_on_commit(db_connection, responder):
    responder.connection = new_connection()
    # pylint: disable-next=protected-access
    responder._insert("salt/minion/1/start", {"value": 1})
    responder.connection.close()
    with patch("mgr_events.psycopg2") as mock_psycopg2:
        mock_psycopg2.connect.return_value = db_connection
        responder.attempt_commit()
    responder.connection.commit()
    responder.cursor.execute("SELECT * FROM suseSaltEvent")
    resp = responder.cursor.fetchall()
    assert len(resp) == 1


# pylint: disable-next=redefined-outer-name,unused-argument
def test_insert_start_event(responder, db_connection):
    responder.event_bus.unpack.return_value = ("salt/minion/12345/start", {"value": 1})
    responder.add_event_to_queue("")
    responder.cursor.execute("SELECT * FROM suseSaltEvent;")
    resp = responder.cursor.fetchall()
    assert resp
    assert responder.tokens == DEFAULT_COMMIT_BURST - 1


# pylint: disable-next=redefined-outer-name
def test_insert_job_return_event(responder):
    responder.event_bus.unpack.return_value = ("salt/job/12345/ret/6789", {"value": 1})
    responder.add_event_to_queue("")
    responder.cursor.execute("SELECT * FROM suseSaltEvent;")
    resp = responder.cursor.fetchall()
    assert resp
    assert responder.tokens == DEFAULT_COMMIT_BURST - 1


# pylint: disable-next=redefined-outer-name
def test_insert_batch_start_event(responder):
    responder.event_bus.unpack.return_value = ("salt/batch/12345/start", {"value": 1})
    responder.add_event_to_queue("")
    responder.cursor.execute("SELECT * FROM suseSaltEvent;")
    resp = responder.cursor.fetchall()
    assert resp
    assert responder.tokens == DEFAULT_COMMIT_BURST - 1


# pylint: disable-next=redefined-outer-name
def test_discard_batch_presence_ping_event(responder):
    responder.event_bus.unpack.return_value = (
        "salt/job/12345/ret/6789",
        {"value": 1, "fun": "test.ping", "metadata": {"batch-mode": True}},
    )
    responder.add_event_to_queue("")
    responder.cursor.execute("SELECT * FROM suseSaltEvent;")
    resp = responder.cursor.fetchall()
    assert len(resp) == 0


# pylint: disable-next=redefined-outer-name
def test_keep_presence_ping_event_without_batch(responder):
    responder.event_bus.unpack.return_value = (
        "salt/job/12345/ret/6789",
        {"value": 1, "fun": "test.ping", "id": "testminion"},
    )
    responder.add_event_to_queue("")
    responder.cursor.execute("SELECT * FROM suseSaltEvent;")
    resp = responder.cursor.fetchall()
    assert len(resp) == 1


# pylint: disable-next=redefined-outer-name
def test_commit_scheduled_on_init(responder):
    assert responder.event_bus.io_loop.call_later.call_count == 1


# pylint: disable-next=redefined-outer-name
def test_commit_empty_queue(responder):
    responder.counters = [0, 0, 0, 0]
    with patch.object(responder, "event_bus", MagicMock()):
        with patch.object(responder, "connection") as mock_connection:
            mock_connection.closed = False
            responder.attempt_commit()
            assert responder.connection.commit.call_count == 0
        assert responder.tokens == DEFAULT_COMMIT_BURST


# pylint: disable-next=redefined-outer-name
def test_postgres_notification(responder):
    with patch.object(responder, "cursor"):
        # pylint: disable-next=protected-access
        responder._insert("salt/minion/1/start", {"value": 1, "id": "testminion"})
        assert responder.counters == [0, 0, 0, 0]
        assert responder.tokens == DEFAULT_COMMIT_BURST - 1
        assert responder.cursor.execute.mock_calls[-1:] == [
            call("NOTIFY suseSaltEvent, '0,0,1,0';")
        ]


# pylint: disable-next=redefined-outer-name
def test_add_token(responder):
    responder.tokens = 0
    responder.add_token()
    assert responder.tokens == 1


# pylint: disable-next=redefined-outer-name
def test_add_token_max(responder):
    responder.add_token()
    assert responder.tokens == DEFAULT_COMMIT_BURST


# pylint: disable-next=redefined-outer-name
def test_commit_avoidance_without_tokens(responder):
    with patch.object(responder, "cursor"):
        with patch.object(responder, "connection") as mock_connection:
            mock_connection.closed = False
            mock_connection.encoding = "utf-8"
            responder.tokens = 0
            # pylint: disable-next=protected-access
            responder._insert("salt/minion/1/start", {"id": "testminion", "value": 1})
            assert responder.counters == [0, 0, 1, 0]
            assert responder.tokens == 0
            assert responder.connection.commit.call_count == 0
            assert responder.cursor.execute.mock_calls == [
                call(
                    "INSERT INTO suseSaltEvent (minion_id, data, queue) VALUES (%s, %s, %s);",
                    (
                        "testminion",
                        '{"tag": "salt/minion/1/start", "data": {"id": "testminion", "value": 1}}',
                        2,
                    ),
                )
            ]


# pylint: disable-next=redefined-outer-name
def test_postgres_connect(db_connection, responder):
    disposable_connection = new_connection()
    disposable_connection.close()
    responder.connection = disposable_connection
    with patch("mgr_events.time") as mock_time:
        with patch("mgr_events.psycopg2") as mock_psycopg2:
            mock_psycopg2.connect.side_effect = [
                psycopg2.OperationalError,
                db_connection,
            ]
            mock_psycopg2.OperationalError = psycopg2.OperationalError
            responder.db_keepalive()
            assert mock_psycopg2.connect.call_count == 2
    mock_time.sleep.assert_called_once_with(5)


# pylint: disable-next=redefined-outer-name
def test_postgres_connect_with_port(responder):
    responder.config["postgres_db"]["port"] = "1234"
    with patch("mgr_events.psycopg2") as mock_psycopg2:
        # pylint: disable-next=protected-access
        responder._connect_to_database()
        mock_psycopg2.connect.assert_called_once_with(
            "dbname='tests' user='postgres' host='localhost' port='1234' password=''"
        )
