"""
Test SQL/db features that fall between low-layer connectionless SQL validation (as in test_statements) and higher-level tests of the view/manual interfaces.

I prefer doing as little work here as possible; hopefully just testing how we handle/contain/recover database errors/exceptions.
"""

import unittest
import sqlite3

from .. import db as doxygen_db
from .. import exceptions
from .. import sql
from . import TEST_DB

db = doxygen_db.DoxygenSQLite3(TEST_DB)


class TestDatabase(unittest.TestCase):
    """Validate statements generated by the Statement class"""

    def test_undefined_table(self):
        with self.assertRaises(exceptions.MalformedQuery):
            sql.Statement(db).table("non_table", "id").prepare()()

    def test_arg_count(self):
        statement = sql.Statement(db).table("def", "rowid").where(kind=None).prepare()

        with self.assertRaises(exceptions.StatementArgumentMismatch):
            statement()

        self.assertEqual(statement("one").fetchall(), [])

        with self.assertRaises(exceptions.StatementArgumentMismatch):
            statement("one", "two")

    def test_decodability(self):
        """
        Common string gotchas in C++ can lead to junk data in the database. One sign of junk data like this is a UTF-8 decode error from sqlite3, but this only helps us if we actually force the dbapi to load the bad data. Here's a sample error:

            sqlite3.OperationalError: Could not decode to UTF-8 column 'name' with text '_����'

        This test attempts to hedge against this by running `select *` on every table.

        - we have to exhaust each query iterator to force the conversions
        - but we don't have to do anything with the row
        - at least for now, I DO grab each row's first column to make hunting the error easier; if the test set grew large enough this could be cut to shave off some runtime.



        Note: This isn't a silver bullet; I think there's >0 probability some junk data decodes cleanly by chance.

        A whole row shouldn't be junk. While it *could* be a whole column, that's easier to spot. More likely, some *fraction* of a text column, usually entered under the same condition, are junk. I imagine the odds of *every* junk-data field being decodable approach 0 as a given table grows.

        A larger database might add confidence here, but some of the other tests don't scale well as size increases. *If this test actually fails to catch junk data, it may make sense to run it against a separate database.*

        Here's a list of all text-bearing tables (select name from sqlite_master WHERE type='table' AND sql like '%TEXT%';) and their current record count when run against doxygen/examples:

            meta                1
            xrefs               9
            param              31
            file               34
            compounddef        89
            memberdef         125
            refid             228
        """
        for table in db.connection.execute(
            "SELECT name FROM sqlite_master WHERE type='table';"
        ):
            # track progress for reporting
            last_good_row = None
            try:
                # exhaust to force decode
                for x in db.connection.execute(
                    "select * from {} order by rowid;".format(table.name)
                ):
                    # x[0] will always work on our tuples; in most cases it'll be the rowid.
                    last_good_row = x[0]
            except sqlite3.OperationalError as e:
                if last_good_row:
                    self.fail(
                        "Exhausting '{}' table caused the following exception after row '{}': \n\n\t{}: {}".format(
                            table.name, last_good_row, e.__class__.__name__, e
                        )
                    )
                else:
                    self.fail(
                        "Exhausting '{}' table caused the following exception: \n\n\t{}: {}".format(
                            table.name, e.__class__.__name__, e
                        )
                    )


class TestTypes(unittest.TestCase):
    """Test the typedef API."""

    def test_undefined_typedef(self):
        with self.assertRaises(exceptions.RequiredTypeMissing):
            db.types.get("blah")

    def test_defined_typedef(self):
        self.assertTrue(db.types.get("member"))

    # TODO: test handling of
    # sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 0 supplied.
