# This file is part of Buildbot.  Buildbot is free software: you can
# redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright Buildbot Team Members

from __future__ import annotations

from twisted.internet import defer

from buildbot.db.test_results import TestResultModel
from buildbot.test.fakedb.base import FakeDBComponent
from buildbot.test.fakedb.row import Row


class TestName(Row):
    table = 'test_names'

    id_column = 'id'
    foreignKeys = ('builderid',)
    required_columns = ('builderid', 'name')

    def __init__(self, id=None, builderid=None, name='nam'):
        super().__init__(id=id, builderid=builderid, name=name)


class TestCodePath(Row):
    table = 'test_code_paths'

    id_column = 'id'
    foreignKeys = ('builderid',)
    required_columns = ('builderid', 'path')

    def __init__(self, id=None, builderid=None, path='path/to/file'):
        super().__init__(id=id, builderid=builderid, path=path)


class TestResult(Row):
    table = 'test_results'

    id_column = 'id'
    foreignKeys = ('builderid', 'test_result_setid', 'test_nameid', 'test_code_pathid')
    required_columns = ('builderid', 'test_result_setid', 'value')

    def __init__(
        self,
        id=None,
        builderid=None,
        test_result_setid=None,
        test_nameid=None,
        test_code_pathid=None,
        line=None,
        duration_ns=None,
        value=None,
    ):
        super().__init__(
            id=id,
            builderid=builderid,
            test_result_setid=test_result_setid,
            test_nameid=test_nameid,
            test_code_pathid=test_code_pathid,
            line=line,
            duration_ns=duration_ns,
            value=value,
        )


class FakeTestResultsComponent(FakeDBComponent):
    def setUp(self):
        self.results = {}
        self.code_paths = {}
        self.names = {}

    def insert_test_data(self, rows):
        for row in rows:
            if isinstance(row, TestName):
                self.names[row.id] = row.values.copy()

        for row in rows:
            if isinstance(row, TestCodePath):
                self.code_paths[row.id] = row.values.copy()

        for row in rows:
            if isinstance(row, TestResult):
                if row.test_nameid is not None:
                    assert row.test_nameid in self.names
                if row.test_code_pathid is not None:
                    assert row.test_code_pathid in self.code_paths

                self.results[row.id] = row.values.copy()

    def _add_code_paths(self, builderid, paths):
        path_to_id = {}

        for path in sorted(paths):
            id = self._get_code_path_id(builderid, path)
            if id is not None:
                path_to_id[path] = id
                continue

            id = Row.nextId()
            self.code_paths[id] = {'builderid': builderid, 'path': path}
            path_to_id[path] = id

        return path_to_id

    def _get_code_path_id(self, builderid, path):
        for id, path_dict in self.code_paths.items():
            if path_dict['builderid'] == builderid and path_dict['path'] == path:
                return id
        return None

    def _add_names(self, builderid, names):
        name_to_id = {}

        for name in sorted(names):
            id = self._get_name_id(builderid, name)
            if id is not None:
                name_to_id[name] = id
                continue

            id = Row.nextId()
            self.names[id] = {'builderid': builderid, 'name': name}
            name_to_id[name] = id

        return name_to_id

    def _get_name_id(self, builderid, name):
        for id, name_dict in self.names.items():
            if name_dict['builderid'] == builderid and name_dict['name'] == name:
                return id
        return None

    @defer.inlineCallbacks
    def addTestResults(self, builderid, test_result_setid, result_values):
        insert_code_paths = set()
        insert_names = set()
        for result_value in result_values:
            if 'value' not in result_value:
                raise KeyError('Each of result_values must contain \'value\' key')

            if 'test_name' not in result_value and 'test_code_path' not in result_value:
                raise KeyError(
                    'Each of result_values must contain at least one of '
                    '\'test_name\' or \'test_code_path\' keys'
                )

            if 'test_name' in result_value:
                insert_names.add(result_value['test_name'])
            if 'test_code_path' in result_value:
                insert_code_paths.add(result_value['test_code_path'])

        code_path_to_id = yield self._add_code_paths(builderid, insert_code_paths)
        name_to_id = yield self._add_names(builderid, insert_names)

        for result_value in result_values:
            insert_value = {
                'value': result_value['value'],
                'builderid': builderid,
                'test_result_setid': test_result_setid,
                'test_nameid': None,
                'test_code_pathid': None,
                'duration_ns': None,
                'line': None,
            }

            if 'test_name' in result_value:
                insert_value['test_nameid'] = name_to_id[result_value['test_name']]
            if 'test_code_path' in result_value:
                insert_value['test_code_pathid'] = code_path_to_id[result_value['test_code_path']]
            if 'line' in result_value:
                insert_value['line'] = result_value['line']
            if 'duration_ns' in result_value:
                insert_value['duration_ns'] = result_value['duration_ns']

            self.results[Row.nextId()] = insert_value

    # returns a Deferred
    def getTestNames(self, builderid, name_prefix=None, result_spec=None):
        ret = []
        for _, row in sorted(self.names.items()):
            if row['builderid'] != builderid:
                continue
            if name_prefix is not None and not row['name'].startswith(name_prefix):
                continue
            ret.append(row['name'])

        if result_spec is not None:
            ret = self.applyResultSpec(ret, result_spec)

        return defer.succeed(ret)

    # returns a Deferred
    def getTestCodePaths(self, builderid, path_prefix=None, result_spec=None):
        ret = []
        for _, row in sorted(self.code_paths.items()):
            if row['builderid'] != builderid:
                continue
            if path_prefix is not None and not row['path'].startswith(path_prefix):
                continue
            ret.append(row['path'])

        if result_spec is not None:
            ret = self.applyResultSpec(ret, result_spec)

        return defer.succeed(ret)

    def _model_from_id(self, id: int, row: dict) -> TestResultModel:
        row = row.copy()
        row['id'] = id

        if row['test_nameid'] is not None:
            row['test_name'] = self.names[row['test_nameid']]['name']
        else:
            row['test_name'] = None
        del row['test_nameid']

        if row['test_code_pathid'] is not None:
            row['test_code_path'] = self.code_paths[row['test_code_pathid']]['path']
        else:
            row['test_code_path'] = None
        del row['test_code_pathid']

        return TestResultModel(
            id=row['id'],
            builderid=row['builderid'],
            test_result_setid=row['test_result_setid'],
            line=row['line'],
            duration_ns=row['duration_ns'],
            value=row['value'],
            test_name=row['test_name'],
            test_code_path=row['test_code_path'],
        )

    # returns a Deferred
    def getTestResult(self, test_resultid):
        if test_resultid not in self.results:
            return defer.succeed(None)
        return defer.succeed(self._model_from_id(test_resultid, self.results[test_resultid]))

    # returns a Deferred
    def getTestResults(self, builderid, test_result_setid, result_spec=None):
        ret = []
        for id, row in sorted(self.results.items()):
            if row['builderid'] != builderid:
                continue
            if row['test_result_setid'] != test_result_setid:
                continue
            ret.append(self._model_from_id(id, row))

        if result_spec is not None:
            ret = self.applyResultSpec(ret, result_spec)

        return defer.succeed(ret)
