from django.http import HttpResponse
import json
from collections import OrderedDict

# Common base class for FC
class FusionCharts:

    baseTemplate = """
        <script type="text/javascript">
            FusionCharts.ready(function () {
                __FC__
            });
        </script>"""

    constructorTemplate = """new FusionCharts(__constructorOptions__);"""
    renderTemplate = """FusionCharts("__chartId__").render();"""
    eventTemplate = """FusionCharts("__chartId__").addEventListener("_fceventname_",_fceventbody_);"""

    # constructor
    def __init__(self, type, id, width, height, renderAt, dataFormat, dataSource):
        self.eventOptions = OrderedDict()
        self.constructorOptions = {}
        self.constructorOptions['type'] = type
        self.constructorOptions['id'] = id
        self.constructorOptions['width'] = width
        self.constructorOptions['height'] = height
        self.constructorOptions['renderAt'] = renderAt
        self.constructorOptions['dataFormat'] = dataFormat
        #dataSource = unicode(dataSource, errors='replace')
        self.constructorOptions['dataSource'] = dataSource

    def addEvent(self, eventName, funcName):
        self.eventOptions[eventName] = funcName

    def addMessage(self, messageName, messageValue):
        self.constructorOptions[messageName] = messageValue

    # render the chart created
    # It prints a script and calls the FusionCharts javascript render method of created chart
    def render(self):

        # Serialize constructorOptions to a JSON formatted
        self.readyJson = json.dumps(self.constructorOptions, ensure_ascii=False)

        # Create Fusioncharts constructor from template and insert JSON data in it
        self.readyJson = FusionCharts.constructorTemplate.replace('__constructorOptions__', self.readyJson)

        # Iterate and attach EventHandler from template
        for key, value in self.eventOptions.items():
            self.readyJson = self.readyJson + FusionCharts.eventTemplate.replace('__chartId__', self.constructorOptions['id'])
            self.readyJson = self.readyJson.replace("_fceventname_", key).replace("_fceventbody_", value)

        # FusionCharts Render method will create chart
        self.readyJson = self.readyJson + FusionCharts.renderTemplate.replace('__chartId__', self.constructorOptions['id'])
        self.readyJson = FusionCharts.baseTemplate.replace("__FC__", self.readyJson)

        self.readyJson = self.readyJson.replace('\\n', '')
        self.readyJson = self.readyJson.replace('\\t', '')

        if(self.constructorOptions['dataFormat'] == 'json'):
            self.readyJson = self.readyJson.replace('\\', '')
            self.readyJson = self.readyJson.replace('"{', "{")
            self.readyJson = self.readyJson.replace('}"', "}")

        return self.readyJson


from django.utils import timezone
from django.utils.timezone import timedelta
from django.core.exceptions import ValidationError
from django.db import models
from car_rental.cars.models import Car

class Rental(models.Model):

    STATUS_CHOICES = (
        (0, 'Pendente'),
        (1, 'Aprovada'),
        (2, 'Cancelada')
    )
    status = models.IntegerField(
        'Situação',
        choices=STATUS_CHOICES,
        default=0,
    )

    def get_default_car():
        return Car.objects.first()

    car = models.ForeignKey(
        Car, verbose_name='Carro',
        on_delete=models.CASCADE,
        related_name='rentals',
        default=get_default_car
    )

    pick_up_date = models.DateTimeField(
        'Data de Retirada',
        default=timezone.now()
    )
    drop_off_date = models.DateTimeField(
        'Data de Entrega',
        default=timezone.now() + timedelta(days=1)
    )

    observations = models.TextField('Observações', max_length=100, blank=True)

    created_at = models.DateTimeField('Criado em', auto_now_add=True)
    updated_at = models.DateTimeField('Atualizado em', auto_now=True)

    def __str__(self):
        return("Reserva #" + str(self.pk))

    # def active(self):
    #     self.status = 1
    #     self.save()
    #
    # def cancelled(self):
    #     self.status = 2
    #     self.save()

    # def is_approved(self):
    #     return self.status == 1

    class Meta:
        verbose_name = 'Aluguel'
        verbose_name_plural = 'Aluguéis'
        ordering = ['car','-pick_up_date','drop_off_date']

    def clean(self):
        if not self._state.adding: # caso o user esteja fazendo UPDATE:
            original_instance = Rental.objects.get(pk=self.pk)

            for field in self._meta.fields:
                field_name = field.name
                current_value = getattr(self, field_name)
                original_value = getattr(original_instance, field_name)
                # Só poderá editar o STATUS, e nenhum outro campo
                if field_name != 'status' and current_value != original_value:
                    field_verbose_name = field.verbose_name
                    raise ValidationError(f"O campo '{field_verbose_name}' não pode ser alterado.")
        else:
            self.validate_car_availability()
            self.validate_date()
            self.validate_date_overlap()

    def validate_date(self):
        current_datetime = timezone.now()
        if not self.pick_up_date < self.drop_off_date:
            raise ValidationError('Não foi possível realizar a reserva. \
                                  A data de retirada não pode ser maior que a \
                                  data de devolução.')

        if not self.pick_up_date > current_datetime:
            raise ValidationError('Não foi possível realizar a reserva. \
                                  A data de retirada não pode ser anterior ao \
                                  dia de hoje.')

    def validate_car_availability(self):
        if self.car.status == 2:
            raise ValidationError('Não foi possível realizar a reserva. \
                                  O veículo selecionado está em manutenção. \
                                  Por favor, selecione outro veículo')

    def validate_date_overlap(self):

        existing_rentals = Rental.objects.filter(car=self.car).exclude(status=2)

        for rental in existing_rentals:
            if ((rental.pick_up_date <= self.pick_up_date <= rental.drop_off_date) or
                (rental.pick_up_date <= self.drop_off_date <= rental.drop_off_date) or
                (self.pick_up_date <= rental.drop_off_date <= self.drop_off_date)):

                raise ValidationError('O veículo já está ocupado nessa data. \
                                      Por favor, selecione outra data')


from copy import deepcopy

import eta.core.serial as etas

class Document(object):
    """Base class for objects that are associated with
    :class:`fiftyone.core.dataset.Dataset` instances and are backed by
    documents in database collections.

    Args:
        dataset (None): the :class:`fiftyone.core.dataset.Dataset` to which the
            document belongs
    """

    def __init__(self, dataset=None):
        self._dataset = dataset

    def __dir__(self):
        return super().__dir__() + list(self.field_names)

    def __getattr__(self, name):
        try:
            return super().__getattribute__(name)
        except AttributeError:
            if name == "_doc":
                raise

        return self._doc.get_field(name)

    def __setattr__(self, name, value):
        if name.startswith("_") or (
            hasattr(self, name) and not self._doc.has_field(name)
        ):
            super().__setattr__(name, value)
        else:
            try:
                self._secure_media(name, value)
            except AttributeError:
                pass

            self._doc.__setattr__(name, value)

    def __delattr__(self, name):
        try:
            self.__delitem__(name)
        except KeyError:
            super().__delattr__(name)

    def __delitem__(self, field_name):
        try:
            self.clear_field(field_name)
        except ValueError as e:
            raise KeyError(e.args[0])

    def __copy__(self):
        return self.copy()

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False

        return self._doc == other._doc

    @property
    def id(self):
        """The ID of the document, or ``None`` if it has not been added to the
        database.
        """
        return str(self._doc.id) if self._in_db else None

    @property
    def _id(self):
        """The ObjectId of the document, or ``None`` if it has not been added
        to the database.
        """
        return self._doc.id if self._in_db else None

    @property
    def ingest_time(self):
        """The time the document was added to the database, or ``None`` if it
        has not been added to the database.
        """
        return self._doc.ingest_time

    @property
    def in_dataset(self):
        """Whether the document has been added to a dataset."""
        return self.dataset is not None

    @property
    def dataset(self):
        """The dataset to which this document belongs, or ``None`` if it has
        not been added to a dataset.
        """
        return self._dataset

    @property
    def field_names(self):
        """An ordered tuple of the names of the fields of this document."""
        return self._doc.field_names

    @property
    def _in_db(self):
        """Whether the underlying :class:`fiftyone.core.odm.Document` has
        been inserted into the database.
        """
        return self._doc.in_db

    @property
    def _skip_iter_field_names(self):
        """A tuple of names of fields to skip when :meth:`iter_fields` is
        called.
        """
        return tuple()

    def _get_field_names(self, include_private=False):
        """Returns an ordered tuple of field names of this document.

        Args:
            include_private (False): whether to include private fields

        Returns:
            a tuple of field names
        """
        return self._doc._get_field_names(include_private=include_private)

    def has_field(self, field_name):
        """Determines whether the document has the given field.

        Args:
            field_name: the field name

        Returns:
            True/False
        """
        return self._doc.has_field(field_name)

    def get_field(self, field_name):
        """Gets the value of a field of the document.

        Args:
            field_name: the field name

        Returns:
            the field value

        Raises:
            AttributeError: if the field does not exist
        """
        if field_name == "id":
            return self.id

        return self._doc.get_field(field_name)

    def set_field(self, field_name, value, create=True):
        """Sets the value of a field of the document.

        Args:
            field_name: the field name
            value: the field value
            create (True): whether to create the field if it does not exist

        Raises:
            ValueError: if ``field_name`` is not an allowed field name or does
                not exist and ``create == False``
        """
        if field_name.startswith("_"):
            raise ValueError(
                "Invalid field name: '%s'. Field names cannot start with '_'"
                % field_name
            )

        self._doc.set_field(field_name, value, create=create)

    def update_fields(self, fields_dict, create=True):
        """Sets the dictionary of fields on the document.

        Args:
            fields_dict: a dict mapping field names to values
            create (True): whether to create fields if they do not exist
        """
        for field_name, value in fields_dict.items():
            self.set_field(field_name, value, create=create)

    def clear_field(self, field_name):
        """Clears the value of a field of the document.

        Args:
            field_name: the name of the field to clear

        Raises:
            ValueError: if the field does not exist
        """
        self._doc.clear_field(field_name)

    def iter_fields(self):
        """Returns an iterator over the ``(name, value)`` pairs of the fields
        of the document.

        Private fields are omitted.

        Returns:
            an iterator that emits ``(name, value)`` tuples
        """
        field_names = tuple(
            f for f in self.field_names if f not in self._skip_iter_field_names
        )
        for field_name in field_names:
            yield field_name, self.get_field(field_name)

    def merge(
        self,
        document,
        omit_fields=None,
        omit_none_fields=True,
        overwrite=True,
    ):
        """Merges the fields of the document into this document.

        ``None``-valued fields are always omitted.

        Args:
            document: a :class:`Document` of the same type
            omit_fields (None): an optional list of fields to omit
            omit_none_fields (True): whether to omit ``None``-valued fields of
                the provided document
            overwrite (True): whether to overwrite existing fields. Note that
                existing fields whose values are ``None`` are always
                overwritten
        """
        if omit_fields is not None:
            omit_fields = set(omit_fields)
        else:
            omit_fields = set()

        existing_field_names = self.field_names

        for field_name, value in document.iter_fields():
            if field_name in omit_fields:
                continue

            if omit_none_fields and value is None:
                continue

            if (
                not overwrite
                and (field_name in existing_field_names)
                and (self[field_name] is not None)
            ):
                continue

            self.set_field(field_name, value)

    def copy(self):
        """Returns a deep copy of the document that has not been added to the
        database.

        Returns:
            a :class:`Document`
        """
        kwargs = {k: deepcopy(v) for k, v in self.iter_fields()}
        return self.__class__(**kwargs)

    def to_dict(self):
        """Serializes the document to a JSON dictionary.

        Sample IDs and private fields are excluded in this representation.

        Returns:
            a JSON dict
        """
        d = self._doc.to_dict(extended=True)
        return {k: v for k, v in d.items() if not k.startswith("_")}

    def to_json(self, pretty_print=False):
        """Serializes the document to a JSON string.

        Sample IDs and private fields are excluded in this representation.

        Args:
            pretty_print (False): whether to render the JSON in human readable
                format with newlines and indentations

        Returns:
            a JSON string
        """
        return etas.json_to_str(self.to_dict(), pretty_print=pretty_print)

    def to_mongo_dict(self):
        """Serializes the document to a BSON dictionary equivalent to the
        representation that would be stored in the database.

        Returns:
            a BSON dict
        """
        return self._doc.to_dict(extended=False)

    def save(self):
        """Saves the document to the database."""
        self._doc.save()

    def reload(self):
        """Reloads the document from the database."""
        # only reload attrs that are in our schema
        self._doc.reload(*list(self._doc))

    def _delete(self):
        """Deletes the document from the database."""
        self._doc.delete()

    @classmethod
    def from_dict(cls, d):
        """Loads the document from a JSON dictionary.

        The returned document will not belong to a dataset.

        Returns:
            a :class:`Document`
        """
        doc = cls._NO_COLL_CLS.from_dict(d, extended=True)
        return cls.from_doc(doc)

    @classmethod
    def from_json(cls, s):
        """Loads the document from a JSON string.

        Args:
            s: the JSON string

        Returns:
            a :class:`Document`
        """
        doc = cls._NO_COLL_CL.from_json(s)
        return cls.from_doc(doc)

    def _set_backing_doc(self, doc, dataset=None):
        """Sets the backing doc for the document.

        Args:
            doc: a :class:`fiftyone.core.odm.SampleDocument`
            dataset (None): the :class:`fiftyone.core.dataset.Dataset` to which
                the document belongs, if any
        """
        if not doc.id:
            doc.save()

        self._doc = doc

        samples = self._instances[doc.collection_name]
        if self.id not in samples:
            samples[self.id] = self

        self._dataset = dataset

    def _reset_backing_doc(self):
        """Resets the backing doc for the document.

        The document will no longer belong to a dataset.
        """
        self._doc = self.copy()._doc
        self._dataset = None

    @classmethod
    def _rename_fields(cls, collection_name, field_names, new_field_names):
        """Renames the field on all in-memory documents in the collection.

        Args:
            collection_name: the name of the MongoDB collection
            field_names: an iterable of field names
            new_field_names: an iterable of new field names
        """
        if collection_name not in cls._instances:
            return

        for document in cls._instances[collection_name].values():
            data = document._doc._data
            for field_name, new_field_name in zip(
                field_names, new_field_names
            ):
                data[new_field_name] = data.pop(field_name, None)

    @classmethod
    def _clear_fields(cls, collection_name, field_names):
        """Clears the values for the given field(s) (i.e., sets them to None)
        on all in-memory documents in the collection.

        Args:
            collection_name: the name of the MongoDB collection
            field_names: an iterable of field names
        """
        if collection_name not in cls._instances:
            return

        for document in cls._instances[collection_name].values():
            for field_name in field_names:
                document._doc._data[field_name] = None

    @classmethod
    def _purge_fields(cls, collection_name, field_names):
        """Removes the field(s) from all in-memory documents in the collection.

        Args:
            collection_name: the name of the MongoDB collection
            field_names: an iterable of field names
        """
        if collection_name not in cls._instances:
            return

        for document in cls._instances[collection_name].values():
            for field_name in field_names:
                document._doc._data.pop(field_name, None)

    @classmethod
    def _reload_docs(cls, collection_name, doc_ids=None):
        """Reloads the backing documents for all in-memory documents in the
        collection.

        Documents that are still in the collection will be reloaded, and
        documents that are no longer in the collection will be reset.

        Args:
            collection_name: the name of the MongoDB collection
            doc_ids (None): a list of IDs of documents that are still in the
                collection. If not provided, all documents are assumed to still
                be in the collection
        """
        if collection_name not in cls._instances:
            return

        documents = cls._instances[collection_name]

        # Reload all docs
        if doc_ids is None:
            for document in documents.values():
                document.reload()

        # Reload docs with `doc_ids`, reset others
        if doc_ids is not None:
            reset_ids = set()
            for document in documents.values():
                if document.id in doc_ids:
                    document.reload()
                else:
                    reset_ids.add(document.id)
                    document._reset_backing_doc()

            for doc_id in reset_ids:
                documents.pop(doc_id, None)

    @classmethod
    def _reset_docs(cls, collection_name, doc_ids=None):
        """Resets the backing documents for in-memory documents in the
        collection.

        Reset documents will no longer belong to their parent dataset.

        Args:
            collection_name: the name of the MongoDB collection
            doc_ids (None): an optional list of document IDs to reset. By
                default, all documents are reset
        """
        if collection_name not in cls._instances:
            return

        # Reset all docs
        if doc_ids is None:
            documents = cls._instances.pop(collection_name)
            for document in documents.values():
                document._reset_backing_doc()

        # Reset docs with `doc_ids`
        if doc_ids is not None:
            documents = cls._instances[collection_name]

            for doc_id in doc_ids:
                document = documents.pop(doc_id, None)
                if document is not None:
                    document._reset_backing_doc()


from django.core.validators import MaxValueValidator, MinValueValidator
from django.urls import reverse
from django.db import models
from django.utils.text import slugify
from egyon import settings
from django.forms.models import ModelForm
from django.forms.widgets import CheckboxSelectMultiple
from django.template.defaultfilters import truncatechars

class Vendor(models.Model):
    Name = models.CharField(max_length=250, blank=True, unique=True,null = True)
    vendor_slug = models.SlugField(unique=True, db_index=True)
    address = models.CharField(max_length=250, blank=True,null = True,)
    Longitude = models.FloatField(default=0,null=True, blank= True)
    Latitude = models.FloatField(default=0,null=True, blank= True)
    delivery_fees = models.FloatField()
    created = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(default=True)

    def __str__(self):
        return str(self.Name)
    class Meta:
        verbose_name_plural = "Vendors"

class Category(models.Model):
    Category_name = models.CharField(max_length=250,unique = True,)
    display_name =  models.CharField(max_length=250, blank=True,null = True)
    categoryslug = models.SlugField(unique=True, db_index=True,blank=True,null = True)
    image = models.ImageField(
        upload_to="Categories", blank=True,null = True )
    active = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)
    Vendor = models.ManyToManyField(
        Vendor, blank=True,)

    def __str__(self):
        return str(self.Category_name)

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

    def get_absolute_url_category(self):
        return reverse('categories_and_products:category_details', args=[self.Restaurant.restaurant_slug] + [self.categoryslug])

    class Meta:
        verbose_name_plural = "Categories"

class Product(models.Model):
    name = models.CharField(max_length=250, blank=True)
    ArabicName = models.CharField(max_length = 250, blank = True, null= True)
    productslug = models.SlugField(unique=True, db_index=True,)
    Vendor = models.ForeignKey(
        Vendor, on_delete=models.CASCADE, blank=True,null= True)
    description = models.TextField(blank=True)
    price = models.FloatField(default=0)

    category = models.ForeignKey(Category, on_delete=models.CASCADE,blank=True,null = True,)
    boughtPrice = models.FloatField(blank=True, null=True, default=0)
    offerPercentage = models.FloatField(blank=True, null=True,)
    active = models.BooleanField(default=True)
    Most_Popular = models.BooleanField(default=False)
    New_Products = models.BooleanField(default=False)
    Best_Offer = models.BooleanField(default=False)
    created = models.DateTimeField(auto_now_add=True)
    limit = models.IntegerField(default= 0, blank = True, null = True)

    def __str__(self):
        arabic_string = self.ArabicName
        arabic_string.encode('unicode-escape')
        return arabic_string


"""
Library for scraping news on "Le Monde".
Get the HTML of articles and extract some features (see documentation).
Can load and save the HTML and features files.
"""

import urllib.request
import bs4
import os
import json
import collections

def getArticleLinksFromHomePage(links_limit=-1):
    """Scrap the "Le Monde" home page to find all the article links.
    "Le Monde" home page: "http://www.lemonde.fr".
    Return a list of links.

    Don't seek subscribed edition articles.
    Remove duplicate links.

    Keyword arguments:
    links_limit -- number of links maximal to return, if -1 return all
    (default -1)
    """
    lemonde_url = "http://www.lemonde.fr"
    lemonde = bs4.BeautifulSoup(urllib.request.urlopen(lemonde_url).read(),
                                "lxml")
    links = []
    for a in lemonde('a'):
        if (len(links) == links_limit):
            break
        if len(a('', {'class': 'marqueur_restreint'})) == 0 \
                and "/article/" in a['href']:
            links.append(lemonde_url + a['href'])

    # Remove duplicate links
    links = sorted( collections.Counter( links ) )

    return links

def getHtmlArticleFromArticleLinks(links):
    """Scrap an Article and return a list of HTML code as string.

    Keyword arguments:
    links -- A list with all the article links to scrap
    """
    html_articles = []
    for link in links:
        html_articles.append(bs4.BeautifulSoup(
            urllib.request.urlopen(link).read(), "lxml").prettify())
    return html_articles

def extractFeaturesFromHtmlArticles(html_articles):
    """Return a list of json like object with all the features.
    Extract those features from the article:
     - Title
     - URL
     - Article category
     - Writer
     - Article description
     - Publish Time
     - Update Time
     - Article content
     - Related article titles

     If a feature is not found, an empty string is set instead.
     Don't return features where something is missing.

    Keyword arguments:
    html_articles -- A list of html articles in string type
    """
    features_articles = []
    for index, html in enumerate(html_articles):
        missing = ""
        features = {}
        article_lemonde = bs4.BeautifulSoup(html, "lxml")

        # Partial article due to the limit for subscribed edition
        if len(article_lemonde('div', {'class', 'block-teaser'})) > 0:
            continue

        # Title
        if len(article_lemonde('h1', {'class': 'tt2'})) > 0:
            features['title'] = article_lemonde(
                'h1', {'class': 'tt2'})[0].text.strip()
        elif len(article_lemonde('h1', {'itemprop': 'Headline'})) > 0:
            features['title'] = article_lemonde(
                'h1', {'itemprop': 'Headline'})[0].text.strip()
        else:
            features['title'] = ""
            missing = missing + "- title missing\n"

        # Simple scrap
        if len(article_lemonde('', {'class': 'description-article'})) > 0:
            features['article_description'] = article_lemonde(
                '', {'class': 'description-article'})[0].text
        else:
            features['article_description'] = ""
            missing = missing + "- article_description missing\n"

        if len(article_lemonde('div', {'itemprop': 'articleBody'})) > 0:
            features['article_content'] = article_lemonde(
                'div', {'itemprop': 'articleBody'})[0].text.strip()
        else:
            features['article_content'] = ""
            missing = missing + "- article_content missing\n"

        related_articles = []
        if len(article_lemonde('aside', {'class': 'bloc_base meme_sujet'})) > 0:
            for art in article_lemonde('aside', {'class': 'bloc_base meme_sujet'})[0]('a'):
                related_articles.append(art.text.strip())
            features['related_articles'] = related_articles
        # Alternative method for some pages
        elif len(article_lemonde('div', {'class': 'related-articles'})) > 0:
            for art in article_lemonde('div', {'class': 'related-articles'})[0]('a'):
                related_articles.append(art.text.strip())
            features['related_articles'] = related_articles
        else:
            features['related_articles'] = []
            missing = missing + "- related_articles missing\n"

        # Category
        if len(article_lemonde('meta', {'property': 'og:url'})) > 0:
            offset = len("http://www.lemonde.fr")
            lemonde_article_url = article_lemonde(
                'meta', {'property': 'og:url'})[0]['content']
            features['url'] = lemonde_article_url
            slash1 = lemonde_article_url.find('/', offset) + 1
            slash2 = lemonde_article_url.find('/', slash1) + 1
            if (slash1 >= 0 and slash2 >= 0):
                features['category'] = lemonde_article_url[slash1:slash2 - 1]
        else:
            features['category'] = ""
            missing = missing + "- category missing\n"

        # Writer
        if len(article_lemonde('span', {'itemprop': 'Publisher'})) > 0:
            features['writer'] = article_lemonde(
                'span', {'itemprop': 'Publisher'})[0].text.strip()
        else:
            features['writer'] = ""
            missing = missing + "- writer missing\n"

        # Date
        if len(article_lemonde('time', {'itemprop': 'datePublished'})) > 0:
            features['publish_time'] = article_lemonde(
                'time', {'itemprop': 'datePublished'})[0]['datetime']
        else:
            features['publish_time'] = ""
            missing = missing + "- publish_time missing\n"

        if len(article_lemonde('time', {'itemprop': 'dateModified'})) > 0:
            features['update_time'] = article_lemonde(
                'time', {'itemprop': 'dateModified'})[0]['datetime']
        else:
            features['update_time'] = "" # Can be often, not a big problem

        if len( missing ) > 0:
            print ("---------------")
            print ("HTML page index " + str(index))
            print ( missing )
        else:
            features_articles.append(features)

    return features_articles

def saveArticlesAsHtml(html_articles,
                       url_articles,
                       location="data/html/"):
    """Save the html code on the disk.
    The html file name is the article title.

    Keyword arguments:
    html_articles -- A list of html articles in string type
    url_articles -- List of urls, the ones related to the html page
    (same size as html_articles)
    location -- where to save all the files (default: data/html/)
    """
    if not os.path.exists(location):
        os.makedirs(location)

    for html, url in zip(html_articles, url_articles):
        last_slash = url[::-1].find("/")
        html_file = location + url[len(url) - last_slash:]
        f = open(html_file, "w")
        f.write(html)
        f.close()

def loadArticlesAsHtml(location):
    """Load the html code from the disk.
    Return a list of html articles in string type.

    Keyword arguments:
    location -- where to open all the files
    """
    if not os.path.exists(location):
        raise ValueError(
            "Path (" + location + ") to find html pages doesn't exist.")

    if location[len(location) - 1] is not "/":
        location = location + "/"

    html_articles = []
    for f_html in os.listdir(location):
        if ".html" in f_html:
            f = open(location + f_html, "r")
            html_articles.append(f.read())
            f.close()
    return html_articles

def saveFeaturesArticlesAsJson(features_articles,
                               location="data/features/"):
    """Save the features json like object from each article on the disk.

    Keyword arguments:
    features_articles -- A list features json like object
    location -- where to save all the files (default: data/features/)
    """
    if not os.path.exists(location):
        os.makedirs(location)

    for feat in features_articles:
        last_slash = feat['url'][::-1].find("/")
        json_file = location + \
            feat['url'][len(feat['url']) - last_slash:].replace("html", "json")
        f = open(json_file, "w")
        f.write(json.dumps(feat))
        f.close()

def loadFeaturesArticlesAsJson(location):
    """Load the features json like object from the disk.
    Return a list of features json like object.

    Keyword arguments:
    location -- where to open all the files
    """
    if not os.path.exists(location):
        raise ValueError(
            "Path (" + location + ") to find json features doesn't exist.")

    if location[len(location) - 1] is not "/":
        location = location + "/"

    features_articles = []
    for f_json in os.listdir(location):
        if ".json" in f_json:
            f = open(location + f_json, "r")
            features_articles.append(json.loads(f.read()))
            f.close()
    return features_articles

def scrapLeMonde(links_limit=-1,
                 save_html=False,
                 save_features_json=False,
                 location="data"):
    """Scrap articles from Le Monde and extract features.

    Returns:
    list of fetched URLs
    list of HTML code as string fetched
    list of extracted features from the HTML

    Keyword arguments:
    links_limit -- number of links maximal to return, if -1 return all
    (default -1)
    subscribed_edition -- if subscribed article links should be return
    ( default false)
    save_html -- if save html on disk,
    save_features_json -- if save features on disk
    """
    links = getArticleLinksFromHomePage(links_limit=links_limit)

    htmls = getHtmlArticleFromArticleLinks(links)

    if ( save_html ):
        saveArticlesAsHtml(htmls, links, location=location+"/html/")

    features = extractFeaturesFromHtmlArticles( htmls )

    if ( save_features_json ):
        saveFeaturesArticlesAsJson(features, location=location+"/features/")

    return links, htmls, features


import copy
import os.path as osp
import os
import mmcv
import numpy as np
import torch

from mmdet.datasets.builder import DATASETS
from mmdet.datasets.custom import CustomDataset
vertices = np.array([[0.5,-0.5,0.5],[-0.5,0.5,0.5],[0.5,0.5,-0.5],[0.5,-0.5,-0.5],[-0.5,-0.5,0.5],
                     [-0.5,0.5,-0.5],[-0.5,-0.5,-0.5],[0.5,0.5,0.5]])

def corners_nd(dims, origin=0.5):
    """Generate relative box corners based on length per dim and origin point.

    Args:
        dims (np.ndarray, shape=[N, ndim]): Array of length per dim
        origin (list or array or float, optional): origin point relate to
            smallest point. Defaults to 0.5

    Returns:
        np.ndarray, shape=[N, 2 ** ndim, ndim]: Returned corners.
        point layout example: (2d) x0y0, x0y1, x1y0, x1y1;
            (3d) x0y0z0, x0y0z1, x0y1z0, x0y1z1, x1y0z0, x1y0z1, x1y1z0, x1y1z1
            where x0 < x1, y0 < y1, z0 < z1.
    """
    ndim = int(dims.shape[1])
    corners_norm = np.stack(
        np.unravel_index(np.arange(2**ndim), [2] * ndim),
        axis=1).astype(dims.dtype)
    # now corners_norm has format: (2d) x0y0, x0y1, x1y0, x1y1
    # (3d) x0y0z0, x0y0z1, x0y1z0, x0y1z1, x1y0z0, x1y0z1, x1y1z0, x1y1z1
    # so need to convert to a format which is convenient to do other computing.
    # for 2d boxes, format is clockwise start with minimum point
    # for 3d boxes, please draw lines by your hand.
    if ndim == 2:
        # generate clockwise box corners
        corners_norm = corners_norm[[0, 1, 3, 2]]
    elif ndim == 3:
        corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]]
    corners_norm = corners_norm - np.array(origin, dtype=dims.dtype)
    corners = dims.reshape([-1, 1, ndim]) * corners_norm.reshape(
        [1, 2**ndim, ndim])
    return corners

def rotation_3d_in_axis(points,
                        angles,
                        axis=0,
                        return_mat=False,
                        clockwise=False):
    """Rotate points by angles according to axis.

    Args:
        points (np.ndarray | torch.Tensor | list | tuple ):
            Points of shape (N, M, 3).
        angles (np.ndarray | torch.Tensor | list | tuple | float):
            Vector of angles in shape (N,)
        axis (int, optional): The axis to be rotated. Defaults to 0.
        return_mat: Whether or not return the rotation matrix (transposed).
            Defaults to False.
        clockwise: Whether the rotation is clockwise. Defaults to False.

    Raises:
        ValueError: when the axis is not in range [0, 1, 2], it will
            raise value error.

    Returns:
        (torch.Tensor | np.ndarray): Rotated points in shape (N, M, 3).
    """
    batch_free = len(points.shape) == 2
    if batch_free:
        points = points[None]

    if isinstance(angles, float) or len(angles.shape) == 0:
        angles = torch.full(points.shape[:1], angles)

    assert len(points.shape) == 3 and len(angles.shape) == 1 \
        and points.shape[0] == angles.shape[0], f'Incorrect shape of points ' \
        f'angles: {points.shape}, {angles.shape}'

    assert points.shape[-1] in [2, 3], \
        f'Points size should be 2 or 3 instead of {points.shape[-1]}'

    rot_sin = torch.sin(angles)
    rot_cos = torch.cos(angles)
    ones = torch.ones_like(rot_cos)
    zeros = torch.zeros_like(rot_cos)

    if points.shape[-1] == 3:
        if axis == 1 or axis == -2:
            rot_mat_T = torch.stack([
                torch.stack([rot_cos, zeros, -rot_sin]),
                torch.stack([zeros, ones, zeros]),
                torch.stack([rot_sin, zeros, rot_cos])
            ])
        elif axis == 2 or axis == -1:
            rot_mat_T = torch.stack([
                torch.stack([rot_cos, rot_sin, zeros]),
                torch.stack([-rot_sin, rot_cos, zeros]),
                torch.stack([zeros, zeros, ones])
            ])
        elif axis == 0 or axis == -3:
            rot_mat_T = torch.stack([
                torch.stack([ones, zeros, zeros]),
                torch.stack([zeros, rot_cos, rot_sin]),
                torch.stack([zeros, -rot_sin, rot_cos])
            ])
        else:
            raise ValueError(f'axis should in range '
                             f'[-3, -2, -1, 0, 1, 2], got {axis}')
    else:
        rot_mat_T = torch.stack([
            torch.stack([rot_cos, rot_sin]),
            torch.stack([-rot_sin, rot_cos])
        ])

    if clockwise:
        rot_mat_T = rot_mat_T.transpose(0, 1)

    if points.shape[0] == 0:
        points_new = points
    else:
        points_new = torch.einsum('aij,jka->aik', points, rot_mat_T)

    if batch_free:
        points_new = points_new.squeeze(0)

    if return_mat:
        rot_mat_T = torch.einsum('jka->ajk', rot_mat_T)
        if batch_free:
            rot_mat_T = rot_mat_T.squeeze(0)
        return points_new, rot_mat_T
    else:
        return points_new

def center_to_corner_box3d(centers,
                           dims,
                           angles=None,
                           origin=(0.5, 1.0, 0.5),
                           axis=1):
    """Convert kitti locations, dimensions and angles to corners.

    Args:
        centers (np.ndarray): Locations in kitti label file with shape (N, 3).
        dims (np.ndarray): Dimensions in kitti label file with shape (N, 3).
        angles (np.ndarray, optional): Rotation_y in kitti label file with
            shape (N). Defaults to None.
        origin (list or array or float, optional): Origin point relate to
            smallest point. Use (0.5, 1.0, 0.5) in camera and (0.5, 0.5, 0)
            in lidar. Defaults to (0.5, 1.0, 0.5).
        axis (int, optional): Rotation axis. 1 for camera and 2 for lidar.
            Defaults to 1.

    Returns:
        np.ndarray: Corners with the shape of (N, 8, 3).
    """
    # 'length' in kitti format is in x axis.
    # yzx(hwl)(kitti label file)<->xyz(lhw)(camera)<->z(-x)(-y)(lwh)(lidar)
    # center in kitti format is [0.5, 1.0, 0.5] in xyz.
    corners = corners_nd(dims, origin=origin)
    # corners: [N, 8, 3]
    if angles is not None:
        corners = torch.tensor(corners)
        angles = torch.tensor(angles)
        corners = rotation_3d_in_axis(corners, angles, axis=axis)
        corners = corners.numpy()

    corners += centers.reshape([-1, 1, 3])
    return corners

def recal_bbox(info):
    P2 = info['calib']['P0']

    repro_recs = []
    # if no annotations in info (test dataset), then return
    if 'annos' not in info:
        return repro_recs

    # Get all the annotation with the specified visibilties.
    ann_dicts = info['annos']
    mask = [(ocld in [0,1,2,3]) for ocld in ann_dicts['occluded']]
    for k in ann_dicts.keys():
        ann_dicts[k] = ann_dicts[k][mask]

    # convert dict of list to list of dict
    ann_recs = []
    for i in range(len(ann_dicts['occluded'])):
        ann_rec = {}
        for k in ann_dicts.keys():
            ann_rec[k] = ann_dicts[k][i]
        ann_recs.append(ann_rec)
    # import pdb; pdb.set_trace()
    bbox = []
    # from PIL import Image
    # aa = Image.open(os.path.join('data/waymo_kitti', info['image']['image_path']))
    # aa.save('debug.png')
    # import cv2
    # image = cv2.imread(os.path.join('data/waymo_kitti', info['image']['image_path']))
    #
    # for i in info['annos']['bbox']:
    #     first_point = (int(i[0]), int(i[1]))
    #     end_point = (int(i[2]), int(i[3]))
    #     cv2.rectangle(image, first_point, end_point, (0, 255, 0), 2)
    #
    #
    # cv2.imwrite('debug.png', image)
    for ann_idx, ann_rec in enumerate(ann_recs):
        # Augment sample_annotation with token information.
        # ann_rec['sample_annotation_token'] = \
        #     f"{info['image']['image_idx']}.{ann_idx}"
        # ann_rec['sample_data_token'] = info['image']['image_idx']
        # sample_data_token = info['image']['image_idx']

        loc = ann_rec['location'][np.newaxis, :]
        dim = ann_rec['dimensions'][np.newaxis, :]
        rot = ann_rec['rotation_y'][np.newaxis, np.newaxis]
        # transform the center from [0.5, 1.0, 0.5] to [0.5, 0.5, 0.5]
        dst = np.array([0.5, 0.5, 0.5])
        src = np.array([0.5, 1.0, 0.5])
        loc = loc + dim * (dst - src)
        gt_bbox_3d = np.concatenate([loc, dim, rot], axis=1).astype(np.float32)

        # Filter out the corners that are not in front of the calibrated
        # sensor.
        corners_3d = center_to_corner_box3d(
            gt_bbox_3d[:, :3],
            gt_bbox_3d[:, 3:6],
            gt_bbox_3d[:, 6], [0.5, 0.5, 0.5],
            axis=1)
        corners_3d = corners_3d[0].T  # (1, 8, 3) -> (3, 8)
        # in_front = np.argwhere(corners_3d[2, :] > 0).flatten()
        # corners_3d = corners_3d[:, in_front]

        # Project 3d box to 2d.
        corner_coords = P2[:3,:3]@corners_3d/np.abs(corners_3d[2,:])

        w_min, h_min, w_max, h_max = corner_coords[0].min(),corner_coords[1].min(),\
                                     corner_coords[0].max(),corner_coords[1].max()
        w_min = max(w_min, 0)
        h_min = max(h_min, 0)
        w_max = min(w_max, 1920)
        h_max = min(h_max, 1280)
        import pdb; pdb.set_trace()
        if w_min>=w_max or h_min>=h_max:
            continue
        else:
            bbox.append([w_min, h_min, w_max, h_max])

    return np.array(bbox)

@DATASETS.register_module()
class KittiDataset(CustomDataset):
    CLASSES = ('Car', 'Pedestrian', 'Cyclist')

    def load_annotations(self, ann_file):
        cat2label = {k: i for i, k in enumerate(self.CLASSES)}
        # load image list from file
        # image_list = mmcv.list_from_file(self.ann_file)
        annos = mmcv.load(self.ann_file)
        root = os.getcwd()
        def s2v(bbox):
            return [bbox[0], bbox[1], bbox[0]+bbox[2], bbox[1]+bbox[3]]

        data_infos = []
        annotations = annos['annotations']
        images = annos['images']
        filename0 = annotations[0]['file_name']
        height, width = annos['images'][0]['height'], annos['images'][0]['width']
        data_info = dict(filename=os.path.join(root,self.img_prefix,filename0), width=width, height=height)
        gt_bboxes = []
        gt_labels = []
        gt_bboxes_ignore = []
        gt_labels_ignore = []
        gt_bboxes.append(s2v(annotations[0]['bbox']))
        gt_labels.append(cat2label[annotations[0]['category_name']])
        for anno in annotations[1:]:
            if anno['file_name'] != filename0:
                data_anno = dict(
                    bboxes=np.array(gt_bboxes, dtype=np.float32).reshape(-1, 4),
                    labels=np.array(gt_labels, dtype=np.long),
                    bboxes_ignore=np.array(gt_bboxes_ignore,
                                           dtype=np.float32).reshape(-1, 4),
                    labels_ignore=np.array(gt_labels_ignore, dtype=np.long))
                data_info.update(ann=data_anno)
                data_infos.append(data_info)
                filename0 = anno['file_name']
                gt_bboxes = []
                gt_labels = []
                gt_bboxes_ignore = []
                gt_labels_ignore = []
                # height, width = image['height'], image['width']
                data_info = dict(filename=os.path.join(root, self.img_prefix, filename0), width=width, height=height)
                gt_bboxes.append(s2v(anno['bbox']))
                gt_labels.append(cat2label[anno['category_name']])
            else:
                gt_bboxes.append(s2v(anno['bbox']))
                gt_labels.append(cat2label[anno['category_name']])
        data_anno = dict(
            bboxes=np.array(gt_bboxes, dtype=np.float32).reshape(-1, 4),
            labels=np.array(gt_labels, dtype=np.long),
            bboxes_ignore=np.array(gt_bboxes_ignore,
                                   dtype=np.float32).reshape(-1, 4),
            labels_ignore=np.array(gt_labels_ignore, dtype=np.long))
        data_info.update(ann=data_anno)
        data_infos.append(data_info)
        return data_infos
        # import pdb; pdb.set_trace()

        # data_infos_pkl = []
        # annos_pkl = mmcv.load(self.ann_file.replace('_mono3d.coco.json','.pkl'))
        # for info in annos_pkl:
        #     filename = os.path.join(root,self.img_prefix,info['image']['image_path'])
        #     height, width = info['image']['image_shape'][:2]
        #     data_info = dict(filename=filename, width=width, height=height)
        #     anno = info['annos']
        #     if len(anno['bbox']) == 0:
        #         continue
        #
        #
        #     data_anno = dict(
        #         bboxes=anno['bbox'],
        #         labels=np.array([cat2label[c] for c in anno['name']], dtype=np.long),
        #         bboxes_ignore=np.array([],
        #                                dtype=np.float32).reshape(-1, 4),
        #         labels_ignore=np.array([], dtype=np.long))
        #     data_info.update(ann=data_anno)
        #     data_infos_pkl.append(data_info)
        # import pdb; pdb.set_trace()
        #
        # data_infos_pkl = []
        # annos_pkl = mmcv.load(self.ann_file.replace('_mono3d.coco.json','.pkl'))
        # for info in annos_pkl:
        #     filename = os.path.join(root,self.img_prefix,info['image']['image_path'])
        #     height, width = info['image']['image_shape'][:2]
        #     data_info = dict(filename=filename, width=width, height=height)
        #     anno = info['annos']
        #     if len(anno['bbox']) == 0:
        #         continue
        #     bboxes = recal_bbox(info)
        #     data_anno = dict(
        #         bboxes=bboxes,
        #         labels=np.array([cat2label[c] for c in anno['name']], dtype=np.long),
        #         bboxes_ignore=np.array([],
        #                                dtype=np.float32).reshape(-1, 4),
        #         labels_ignore=np.array([], dtype=np.long))
        #     data_info.update(ann=data_anno)
        #     data_infos_pkl.append(data_info)
        #
        #
        # return data_infos

        # convert annotations to middle format
        # for image_id in image_list:
        #     filename = f'{self.img_prefix}/{image_id}.png'
        #     image = mmcv.imread(filename)
        #     height, width = image.shape[:2]
        #
        #     data_info = dict(filename=f'{image_id}.png', width=width, height=height)
        #
        #     # load annotations
        #     label_prefix = self.img_prefix.replace('image_2', 'label_2')
        #     lines = mmcv.list_from_file(osp.join(label_prefix, f'{image_id}.txt'))
        #
        #     content = [line.strip().split(' ') for line in lines]
        #     bbox_names = [x[0] for x in content]
        #     bboxes = [[float(info) for info in x[4:8]] for x in content]
        #
        #     gt_bboxes = []
        #     gt_labels = []
        #     gt_bboxes_ignore = []
        #     gt_labels_ignore = []
        #
        #     # filter 'DontCare'
        #     for bbox_name, bbox in zip(bbox_names, bboxes):
        #         if bbox_name in cat2label:
        #             gt_labels.append(cat2label[bbox_name])
        #             gt_bboxes.append(bbox)
        #         else:
        #             gt_labels_ignore.append(-1)
        #             gt_bboxes_ignore.append(bbox)
        #
            # data_anno = dict(
            #     bboxes=np.array(gt_bboxes, dtype=np.float32).reshape(-1, 4),
            #     labels=np.array(gt_labels, dtype=np.long),
            #     bboxes_ignore=np.array(gt_bboxes_ignore,
            #                            dtype=np.float32).reshape(-1, 4),
            #     labels_ignore=np.array(gt_labels_ignore, dtype=np.long))
        #
        #     data_info.update(ann=data_anno)
        #     data_infos.append(data_info)
        #
        # return data_infos


import unittest
import os, glob
import filecmp
from shutil import rmtree, copy2
from scipy.stats import chisquare

import sys
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_PATH)

from random import randint, seed, uniform

import generator as g

# Specify the folderpaths used during testing
tmpfolder = "./tmp_test"
tmpcopyfolder = "./tmp_test_copy"
nonexistingfolder = "./temporary_test_folder_non_existant"

class TestGenerator(unittest.TestCase):

	def setUp(self):
		"""This setUp procedure will be called before every test.
		Ensures that no cnf-files exist in the base folder.
		Ensures that the folders `tmp_test` and `tmp_test2` exists.
		These folders will be used for some tests.

		Creates the file `hidden.solution` in the base folder.
		This file contains the solution "1 -2 3 -4 5 ... 99 -100".

		Additionally, a file `hidden2.solution` will be created.
		This file contains line breaks in the solution.
		The solution itself will be randomly generated.
		For this, variables between 1 and 1000 will be used.
		"""

		for f in glob.glob("./gen*.cnf"):
			os.remove(f)

		os.makedirs(tmpfolder, exist_ok=True)
		os.makedirs(tmpcopyfolder, exist_ok=True)

		with open("hidden.solution", "w") as hidden_sol:
			sol = ""
			for i in range(1, 101):
				if i % 2 == 0:
					sol += "-"
				sol += str(i)+" "
			hidden_sol.write(sol[:-1]) # do not write the last " "

		seed()
		self.hidden2 = [] # The second hidden solution will additionaly be safed in a list.
		with open("hidden2.solution", "w") as hidden_sol:
			sol = ""
			# Count the amount of positive literals in the solution with a counter variable.
			# This measure is used to randomly include line breaks.
			# The beginning of the file `hidden2.solution` might look something like this:
			#    -1
			#    -2
			#    3 -4 -5 -6 7 -8 9 10 11 -12 13 -14 15 16 17 18
			#    19 -20 21 22 23 24 -25 26 27 -28 -29 -30 31 32 -33 -34 35
			#    36 -37 -38 -39 40 41 42 43 44 45 46 -47 -48 49 -50 -51 52
			#    -53
			#    54 -55 -56 57 -58 59 -60 -61 -62 -63 64 65 -66 67 68 69 -70 -71 -72 73 74
			#    -75
			counter = 0
			for i in range(1, 1001):
				# Choose randomly 0 or 1.
				# If rand == 1, the variable i will be added positively to the solution.
				# Otherwise, we will include -i as part of the hidden solution.
				rand = randint(0,1)
				if rand == 1:
					counter += 1
					sol += str(i)+" "
					self.hidden2.append(i)
				else:
					sol += "-"+str(i)+" "
				# Add the random line breaks:
				if counter % 10 == 0:
					sol += "\n"
			hidden_sol.write(sol[:-1]) # do not write the last " "

	def tearDown(self):
		"""Will be executed after every test.
		Ensures that all cnf-files that were generated during the test will be deleted after the test.
		Ensures that all temporary test folder will be deleted.
		Additionally, the files `hidden.solution` and `hidden2.solution` will be deleted.
		"""

		rmtree(tmpfolder)
		rmtree(tmpcopyfolder)
		rmtree(nonexistingfolder, ignore_errors=True) # this folder does only get generated during one test

		os.remove("hidden.solution")
		os.remove("hidden2.solution")

		for f in glob.glob("./gen_*.cnf"):
			os.remove(f)

	def test_okay(self):
		"""Test if the test framework works propertly by doing a very simple test.
		"""

		print(sys._getframe(  ).f_code.co_name)
		self.assertTrue(True)
		print("Test Framework okay.")
		print("Started from " + os.getcwd())

	def test_p_values(self):
		"""In this this test we check if invalid p-values will be declined.
		There are the following sources of error:
		1) A list of p-values was given but the length of this list is smaller than k.
		2) A list of p-values was given but
		   a) some entry is > 1.0, or
		   b) some entry is < 0.0, or
		   c) no entry is > 0.0.
		"""

		print(sys._getframe(  ).f_code.co_name)

		n = 1000
		m = 4000
		o = tmpfolder

		# The standard case is k=3. We pass a p-value list with only two entries.
		# This should raise a ValueError.
		p = [0.3, 0.5]
		with self.assertRaises(ValueError):
			g.main(n, m, p=p, o=o)

		# We also pass a list with too many entries. We expect a ValueError.
		p = [0.5, 0.3, 0.2, 0.1]
		with self.assertRaises(ValueError):
			g.main(n, m, p=p, o=o)

		# What happens when there is a p-value > 1.0? We again expect a ValueError.
		p = [0.3, 1.2, 0.5]
		with self.assertRaises(ValueError):
			g.main(n, m, p=p, o=o)

		# And what if there is a p-value < 0.0?
		p = [0.0, 1.0, -0.5]
		with self.assertRaises(ValueError):
			g.main(n, m, p=p, o=o)

		# Does the programm accept lists where no entry is > 0.0?
		p = [0.0, 0.0, 0.0]
		with self.assertRaises(ValueError):
			g.main(n, m, p=p, o=o)

	def test_valid_parameters(self):
		"""This test veryfies that only valid parameter combinations will be accepted.
		"""

		print(sys._getframe(  ).f_code.co_name)

		o = tmpfolder

		# Test if we can generate files without passing the required positional arguments `n` and `m`.
		with self.assertRaises(TypeError):
			g.main(o=o)

		with self.assertRaises(TypeError):
			g.main(n=30, o=o)

		with self.assertRaises(TypeError):
			g.main(m=90, o=o)

		# Test whether negative n-values will be declined.
		n = -1000
		m = 4000
		with self.assertRaises(ValueError):
			g.main(n, m, o=o)

		# Test whether negative m-values will be declined.
		n = 1000
		m = -4000
		with self.assertRaises(ValueError):
			g.main(n, m, o=o)

		# Test whether k-values smaller or equal than 0 will be declined.
		m = 4000
		k = 0
		with self.assertRaises(ValueError):
			g.main(n, m, k=k, o=o)

		# Check if a non-existing file will be accepted as a hidden solution.
		k = 3
		F = os.path.join(tmpfolder, "hid.sol")
		with self.assertRaises(FileNotFoundError):
			g.main(n, m, k=k, o=o, F=F)

	def calculate_file_name(self, n, m, k=3, s=42):
		"""Given the parameters `n`, `m`, `k` (optional, standard 3), `s` (optional, standard 42),
		this method returns the correct file name of the file generated with these parameters as a string.
		"""
		return f"gen_n{n}_m{m}_k{k}SAT_seed{s}.cnf"

	def test_generated_name(self):
		"""Checks if the generated file have the correct filename.
		Checks if the files are in the correct folders as well.
		"""

		print(sys._getframe(  ).f_code.co_name)

		# First, let's check if the file gets generated in the base folder if no output folder is specified.
		# The files then should be generated in "./".

		# The filename should read "gen_n100_m420_k3SAT_seed42.cnf".
		n = 100
		m = 420
		path = os.path.join('./', self.calculate_file_name(n, m))
		g.main(n, m)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# The filename should read "gen_n100_m420_k6SAT_seed42.cnf".
		n = 100
		m = 420
		k = 6
		path = os.path.join('./', self.calculate_file_name(n, m, k=k))
		g.main(n, m, k=k)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# The filename should read "gen_n999_m9090_k9SAT_seed1909.cnf".
		n = 999
		m = 9090
		k = 9
		s = 1909
		path = os.path.join('./', self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# The parameters `p` and `F` should have no influence on the filename.

		# Here, only the influence of `p` gets tested.
		n = 999
		m = 9090
		k = 9
		p = [0.1*c for c in range(1,k+1)]
		s = 1909
		path = os.path.join('./', self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, p=p, s=s)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# Here, only the `F` parameter gets tested.
		n = 1000
		m = 9090
		k = 9
		F = './hidden2.solution'
		s = 123
		path = os.path.join('./', self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s, F=F)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# Finally, both parameters get tested.
		n = 1000
		m = 9090
		k = 3
		p = [0.25*c for c in range(1,k+1)]
		s = 123
		F = './hidden2.solution'
		path = os.path.join('./', self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, p=p, s=s, F=F)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# Do the files get created properly in the specified output path?

		# Note, that the folder `./tmp_test` already exists (see setUp method).
		# To begin with, it gets tests if the program passes if
		# the output folder gets specified as `./tmp_test`.
		n = 1300
		m = 4201
		k = 5
		p = [0.16, 0.32, 0.64, 0.1, 0.00]
		s = 9
		o = tmpfolder
		path = os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, p=p, s=s, o=o)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# Next, we test what happens if the folder gets specified as `./tmp_test/`
		# (notice the additional / at the end of the string!).
		o = tmpfolder
		g.main(n, m, k=k, s=s, o=o)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

		# Finally, we test what happens if a non-existing folder gets passed.
		# The folder `nonexistingfolder` does not exist.
		o = f'./{nonexistingfolder}/tmp'
		path = os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s, o=o)
		self.assertTrue(os.path.isfile(path))
		os.remove(path)

	def test_reproducibility(self):
		"""Here, it gets tested whether two calls with identical seeds also deliver identical files.
		This test generates one file in `./tmp_test` and copies it to `./tmp_test2`.
		The file then gets generated again in `./tmp_test` and gets compared to the one in `./tmp_test2`.
		"""
		print(sys._getframe(  ).f_code.co_name)

		# First test:
		n = 100
		m = 420

		filename = self.calculate_file_name(n, m)
		path1 = os.path.join(tmpfolder, filename)
		path2 = os.path.join(tmpcopyfolder, filename)

		g.main(n, m, o=tmpfolder)
		copy2(path1, path2)
		g.main(n, m, o=tmpfolder)
		# filecmf.cmp(_,_) checks if two files are identical.
		# However, only statistics (hash values, etc.) get compared.
		# For a byte-by-byte check one has to set shallow=False.
		self.assertTrue(filecmp.cmp(path1, path2))

		# We run a second test including a hidden solution and all parameters.
		n = 1000
		m = 9999
		k = 8
		p = [0.3, 0.1, 0.5, 0.1, 0.3, 0.0, 0.7, 0.01]
		s = 9
		F = './hidden2.solution'

		filename = self.calculate_file_name(n, m, k=k, s=s)
		path1 = os.path.join(tmpfolder, filename)
		path2 = os.path.join(tmpcopyfolder, filename)

		g.main(n, m, k=k, p=p, s=s, F=F, o=tmpfolder)
		copy2(path1, path2)
		g.main(n, m, k=k, p=p, s=s, F=F, o=tmpfolder)
		self.assertTrue(filecmp.cmp(path1, path2))

	def get_header(self, filename, n_lines):
		"""This method reads the first `n_lines` of the file specified in `filename`
		and returns them as a list."""
		lines = []
		with open(filename, 'r') as file:
			for _ in range(n_lines):
				lines.append(file.readline())
		return lines

	def get_all_lines(self, filename):
		"""This method returns all lines of the given file as a list.
		"""
		with open(filename, 'r') as file:
			lines = file.readlines()
		return lines

	def test_header(self):
		"""Here we test whether the files get generated with the correct comment lines at the beginning of the file.
		We also check the parameter line.
		"""

		print(sys._getframe(  ).f_code.co_name)

		# First test case:
		n = 2222
		m = 10000
		k = 2
		p = [0.16, 0.32]
		s = 16
		o = tmpfolder
		path = os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, p=p, s=s, o=o)
		header = self.get_header(path, 6)
		# The header should have the following form:
		#   "c This file was generated by _"
		#   "c Created by calling python3 generator.py -n 2222 -m 6666 -k 2 -p 0.16 0.32 -s 16 -o ./tmp_test"
		#   "c Hidden solution: pos.int pos.int pos.int ..."
		#   "c p-values 0.16 0.32"
		#   "p cnf 2222 6666"
		#   "'int' 'int' 0"
		self.assertRegex(header[0], "^c This file was generated by[\w\s\.]+\n$")
		self.assertRegex(header[1], f"^c Created by calling python3 generator\.py -n {n} -m {m} -k {k} -p 0\.16 0\.32 -s {s} -o {tmpfolder}\n$")
		self.assertRegex(header[2], "^c Hidden solution: ([\d]+ )*[\d]+\n$")
		self.assertRegex(header[3], "^c p-values 0\.16 0\.32\n$")
		self.assertRegex(header[4], "^p cnf 2222 10000\n$")
		self.assertRegex(header[5], "^(-?[1-9][\d]* ){2}0\n$")

		# We also check the case where a hidden solution was delivered.
		n = 100
		m = 4312
		k = 3
		s = 166
		F = './hidden.solution'
		path =  os.path.join('./', self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s, F=F)
		header = self.get_header(path, 6)
		# The header should have the following form:
		#   "c This file was generated by _"
		#   "c Created by calling python3 generator.py -n 1010 -m 4312 -k 3 -s 166 -F ./hidden.solution"
		#   "c Hidden solution: 1 3 5 ... 99"
		#   "c p-values 1.00 1.00 1.00"
		#   "p cnf 1010 4312"
		#   "'int' 'int' 'int' 0"
		self.assertRegex(header[0], "^c This file was generated by[\w\s\.]+\n$")
		self.assertRegex(header[1], f"^c Created by calling python3 generator\.py -n {n} -m {m} -k {k} -s {s} -F \./hidden\.solution\n$")
		# The hidden solution will be checked in the next block of code.
		self.assertRegex(header[3], "^c p-values 1\.00 1\.00 1\.00\n$")
		self.assertRegex(header[4], f"^p cnf {n} {m}\n$")
		self.assertRegex(header[5], "^(-?[1-9][\d]* ){3}0\n$")

		# The line specifying the hidden solution will be split at every whitespace.
		# We then first check the part coming before the actual hidden solution.
		hidden_sol_line = header[2].split()
		self.assertEqual(hidden_sol_line[0], "c")
		self.assertEqual(hidden_sol_line[1], "Hidden")
		self.assertEqual(hidden_sol_line[2], "solution:")
		# Next, we iterate over the part specifying the hidden solution.
		# Recall that the hidden solution is 1 3 5 ... 9 (note that only positive literals get written to this line).
		hid = 1
		for i in range(3, len(hidden_sol_line)):
			self.assertEqual(hidden_sol_line[i], str(hid))
			hid += 2

		# A final, last test for the second randomly generated hidden solution file with an randomly generated p-list.
		n = 1000
		m = 4200
		k = 3
		p = [uniform(0.01, 1.00) for _ in range(k)] # randomly generate a p-list
		F = './hidden2.solution'
		path =  os.path.join('./', self.calculate_file_name(n, m))
		g.main(n, m, F=F)
		header = self.get_header(path, 6)
		# The header should have the following form:
		#   "c This file was generated by _"
		#   "c Created by calling python3 generator.py -n 1000 -m 4200 -F ./hidden2.solution"
		#   "c Hidden solution: {randomly generated}"
		#   "c p-values {randomly generated}"
		#   "p cnf 1000 4200"
		#   "'int' 'int' 'int' 0"
		self.assertRegex(header[0], "^c This file was generated by[\w\s\.]+\n$")
		self.assertRegex(header[1], f"^c Created by calling python3 generator\.py -n {n} -m {m} -F \./hidden2\.solution\n$")
		# The hidden solution will be checked in the next block of code.
		self.assertRegex(header[3], "^c p-values [\s\d\w\.]*\n$")
		self.assertRegex(header[4], "^p cnf 1000 4200\n$")
		self.assertRegex(header[5], "^(-?[1-9][\d]* ){3}0\n$")

		# The line specifying the hidden solution will be split at every whitespace.
		# We then first check the part coming before the actual hidden solution.
		hidden_sol_line = header[2].split()
		self.assertEqual(hidden_sol_line[0], "c")
		self.assertEqual(hidden_sol_line[1], "Hidden")
		self.assertEqual(hidden_sol_line[2], "solution:")
		# Next, we iterate over the part specifying the hidden solution.
		for i in range(3, len(hidden_sol_line)):
			self.assertEqual(hidden_sol_line[i], str(self.hidden2[i-3]))

	def check_clause_width_and_used_variables(self, lines, n, k=3):
		"""This method checks if all clauses given by the `lines` of a generated file
		have width `k`.
		We also check whether every variable (from 1 up to `n`) is used at least once
		and that every clause contains variables between 1 and `n` and ends with a "0".
		"""
		used_vars = set()
		for i in range(5, len(lines)):
			literals = lines[i].rstrip().split()
			# Each time we should discover a clause of width k, i.e., k literals plus a terminating 0.
			self.assertEqual(len(literals), k+1)
			self.assertEqual(literals[-1], "0")

			for l in literals[:-1]:
				lit = abs(int(l))
				self.assertGreaterEqual(lit, 1)
				self.assertLessEqual(lit, n)
				used_vars.add(lit)

		# Here comes the (double) check whether every variable is present in at least one clause:
		self.assertEqual(len(used_vars), n)

		for l in range(1, n+1):
			self.assertIn(l, used_vars)

	def test_parameters_n_m_k(self):
		"""Here we test whether the generated files abide by the boundary conditions,
		i.e., if when the progam gets called with `n`, `m`, and `k`,
		the generated formula is in k-CNF with n variables over m clauses.
		All clauses get checked for proper format.
		We also test the strict assumption (to make all SAT-solvers happy) that
		clauses in the generated file are not separated by empty lines.
		"""

		print(sys._getframe(  ).f_code.co_name)

		# We check the standard case k=3.
		n = 1234
		m = 5182
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m))
		g.main(n, m, o=o)
		lines = self.get_all_lines(path)

		# In the first 5 lines the header can be found.
		# The header, however, gets already checked in test_header.
		# Thus, we can skip the first 5 lines.
		# Our first check will be the number of lines: This number should be m+5.
		self.assertEqual(m+5, len(lines))

		# After that we check every line that contains a clause and make sure that
		# the width of the clause equals k.
		# Simultaneously, we check whether every variable is used at least once
		# and that every variable is between 1 and n.
		self.check_clause_width_and_used_variables(lines, n)

		# We repeat everything with k=5 and a hidden solution.
		n = 100
		m = 4000
		k = 5
		s = 99
		F = "./hidden.solution"
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s, o=o, F=F)
		lines = self.get_all_lines(path)

		# In the first 5 lines the header can be found.
		# The header, however, gets already checked in test_header.
		# Thus, we can skip the first 5 lines.
		# Our first check will be the number of lines: This number should be m+5.
		self.assertEqual(m+5, len(lines))

		# After that we check every line that contains a clause and make sure that
		# the width of the clause equals k.
		# Simultaneously, we check whether every variable is used at least once
		# and that every variable is between 1 and n.
		self.check_clause_width_and_used_variables(lines, n, k=k)

	def convert_hidden_sol_to_set(self, hid_string, n):
		"""This method converts the hidden solution string in a set.
		A literal is in this set if and only if it is part of the hidden solution.
		"""
		hid_list = hid_string.rstrip().split()
		hid = set()
		for i in range(1, n+1):
			if str(i) in hid_list:
				hid.add(str(i))
			else:
				hid.add(str(-i))
		return hid

	def check_if_hidden_solution_sats_clauses(self, hid, lines):
		"""This method checks if a given hidden solution (passed as a set) satisfies
		all clauses specified by the `lines` of the file.
		"""
		for i in range(5, len(lines)):
			# We convert every clause into a list of strings.
			# The last entry of every such list gets removed since it is equal to "0" (the end-of-clause-symbol).
			literals = lines[i].split()[:-1]
			satisfied = False
			for lit in literals:
				if lit in hid:
					satisfied = True # it suffices that one literal in every clause is satisfied
					break
			# if not satisfied:
			#     print(literals)
			#     print(sorted( [int(x) for x in hid], key=abs ))
			self.assertTrue(satisfied)

	def test_hidden_solution(self):
		"""Here we test if the given hidden solutions does indeed
		constitute a satisfying assignment of the generated instance.
		"""

		print(sys._getframe(  ).f_code.co_name)

		# We begin with the standard value of k=3.
		n = 700
		m = 2961
		s = 101010
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m, s=s))
		g.main(n, m, s=s, o=o)
		lines = self.get_all_lines(path)
		# The hidden solution is written in lines[2].
		hid = self.convert_hidden_sol_to_set(lines[2],n)
		self.check_if_hidden_solution_sats_clauses(hid, lines)

		# We also check if this works with a given hidden solution.
		n = 100
		m = 1200
		k = 4
		s = 111
		p = [0.1, 0.2, 0.4, 0.8]
		F = './hidden.solution'
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s, p=p, o=o, F=F)
		lines = self.get_all_lines(path)
		# The hidden solution is written in lines[2].
		hid = self.convert_hidden_sol_to_set(lines[2],n)
		self.check_if_hidden_solution_sats_clauses(hid, lines)

		# A final check with the second hidden solution file.
		n = 1000
		m = 6000
		k = 2
		s = 1919
		F = './hidden2.solution'
		path =  self.calculate_file_name(n, m, k=k, s=s)
		g.main(n, m, k=k, s=s, F=F)
		lines = self.get_all_lines(path)
		# The hidden solution is written in lines[2].
		hid = self.convert_hidden_sol_to_set(lines[2],n)
		self.check_if_hidden_solution_sats_clauses(hid, lines)

	def check_clause_validity(self, lines):
		"""This method checks if the clauses in `lines` are tautologies or improper k-SAT clauses
		(this second part of the test kind of duplicates check_clause_width_and_used_variables).
		"""
		for i in range(5, len(lines)):
			# We convert every clause in a list of strings.
			# The last entry of every list can be removed since it is equal to "0" (the end-of-clause-symbol).
			literals = lines[i].split()[:-1]
			lit_set = set( [ abs((int(s))) for s in literals ] )
			self.assertEqual(len(literals), len(lit_set))

	def test_constraints(self):
		"""We test the following two side constraints:
		1) The generator is not allowed to produce tautological clauses.
		2) The generator has to produce proper k-SAT clauses.
		"""

		print(sys._getframe(  ).f_code.co_name)

		# The higher k and the more clauses are present the liklier it gets
		# to generate a tautological clause or a clause with a double literal,
		# e.g., clauses of the form {x, y, -x} or {x, y, x}.
		n = 100
		m = 1000
		k = 40
		s = 19
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s, o=o)
		lines = self.get_all_lines(path)
		self.check_clause_validity(lines)

		# A second try:
		n = 1000
		m = 2000
		k = 6
		s = 199
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, k=k, s=s, o=o)
		lines = self.get_all_lines(path)
		self.check_clause_validity(lines)

	def calculate_rejected_prob(self, p):
		"""This function calculated the probability that an arbitrary clause that was generated will be
		rejected by the sampling process based on the given p-values.
		"""

		# Python is a zero-based numbering programming languages (one of its few drawbacks in our opinion).
		# The p-vector used in the description of the generator is one-based, i.e., p = (p_1, ..., p_k).
		# Thus, we extract the p-vector from the p-list to not confuse the reader.
		p1 = p[0]
		p2 = p[1]
		p3 = p[2]

		# If a 3-clause C = (a_1, a_2, a_3) gets sampled ramdomly, we can calculate the probability R
		# that C gets rejected by the p-list based sampling as follows:
		# R = Prob[C gets rej. \cap C contains 3 false literals w.r.t. the hidden solution]
		#   + Prob[C gets rej. \cap C contains 2 false literals w.r.t. the hidden solution]
		#   + Prob[C gets rej. \cap C contains 1 false literals w.r.t. the hidden solution]
		#   + Prob[C gets rej. \cap C contains 0 false literals w.r.t. the hidden solution]
		#   = 1 + 3 * (1-p_1) + 3 * (1-p_2) + (1-p_3),
		# since clauses with 3 false literals always get rejected and (3 choose 2) = (3 choose 1) = 3.
		return (1 + 3*(1-p1) + 3*(1-p2) + (1-p3))/8.0

	def calculate_add_probabilities(self, p, W):
		"""This function calculates the probabilities that a clause will be added.

		Fix a complete assignment alpha to all variables of the formula.
		For i = 0, ..., 3 we call a clause that has i literals set to true under alpha an i-clause.
		Given a randomly generated claues, consider the following scheme:
			FFF
			FFT <--
			FTF <--
			FTT
			TFF <--
			TFT
			TTF
			TTT
		It is easy to see that for example
			q_1 := Prob[a 1-clause will be created] = 3/8 (see arrows).
		More generally, letting
			q_i := Prob[a i-clause will be created],
		we have
			q_0 = 1/8,
			q_1 = 3/8,
			q_2 = 3/8,
			q_3 = 1/8.
		Thus
			P_1 := Prob[a 1-clause will be added]
				 = Prob[a 1-clause will be created in round 1 AND this 1-clause gets sampled in round 1]
				   + Prob[a non-1-clause will be created in round 1] * Prob[a 1-clause will be created in round 2 AND this 1-clause gets sampled in round 2]
				   + ...
				 = q_1 * p_1 + [q_0 * 1 + q_1 * (1 - p_1) + q_2 * (1 - p_2) + q_3 * (1 - p_3)] * q_1 * p_1 + ...
				 = q_1 * p_1 + (5/8 - 3/8 * p_2 - 1/8 * p_3) + ...
				 = q_1 * p_1 * sum_{t=0}^{infty} (5/8 - 3/8 * p_2 - 1/8 * p_3)^{t}
				 = 3/8 * p_1 * 1 / (1 - W_1),
		where
			W_i := q_0 + sum_{j=1}^{3} q_j * (1 - p_j).
		In general,
			P_i := Prob[an i-clause will be added]
				 = q_i * p_i * 1 / (1-W).
		"""
		p1 = p[0]
		p2 = p[1]
		p3 = p[2]
		return p1*3/(8.0 * (1-W)), p2*3/(8.0 * (1-W)), p3*1/(8.0 * (1-W))

	def count_p_clauses(self, clauses, hidden_list, total_num_of_vars):
		"""This method counts the number of "correct" literals in the list `clauses` w.r.t. the given `hidden_list`.
		The method needs also `total_num_of_vars`, the total number of variables appearing in the whole formula.
		Note, that this method is only implemented for the 3-CNF case.
		"""

		# We create a dictionary that in the end will contain the number of clauses with 1, 2, or 3 "correct" literals.
		# Nota bene: There cannot be a clause with 0 "correct" literals, as this would mean that the clause (and thus the whole formula)
		# gets falsified. However, we construct satisfiable formulas. This was checked via `test_hidden_solution`.
		clause_count = {1:0, 2:0, 3:0}

		# The passed `hidden_list` will be an incomplete hidden solution in the sense that only positive literals of
		# the underlying hidden solution will be listed.
		# Thus, we convert the list `hidden_list` into a dictionary `hidden_complete_dict` whereby we can identify ne negative literals.
		hidden_complete_dict = {}
		for i in range(1, total_num_of_vars+1):
			if i in hidden_list:
				hidden_complete_dict[i] = 1
			else:
				hidden_complete_dict[i] = -1

		# For each clause check how many "correct" literals it contains.
		for clause in clauses:
			# We convert each clause in a list of strings.
			# The last entry "0" (signaling the end of the line/clause) will be removed.
			literals = clause.split()[:-1]
			count_correct = 0
			for l in literals:
				lit = int(l)
				sign = hidden_complete_dict[abs(lit)]
				# Check if the literal in the clause and the literal in the hidden solution has the same signum.
				# If yes, then the literal is "correct" w.r.t. the hidden solution.
				if lit * sign > 0:
					count_correct += 1

			clause_count[count_correct] += 1

		return [clause_count[1], clause_count[2], clause_count[3]]

	def test_probabilities(self):
		"""This is a probabilistic check (using the chi-squared test) of the clause sampling due to the p-list.
		We only check the standard case k=3.
		"""

		print(sys._getframe(  ).f_code.co_name)

		p = [0.25, 0.125, 0.3]
		n = 1000
		m = 5000
		s = randint(1,10000) # We use a random seed. Repeating this test leads to more confidence in the result.
		o = tmpfolder
		F = './hidden2.solution'
		path =  os.path.join(o, self.calculate_file_name(n, m, s=s))
		g.main(n, m, s=s, o=o, p=p, F=F)
		lines = self.get_all_lines(path)

		# We count how many times a 1-correct, 2-correct, and 3-correct clause appears.
		# We compare these values to the expected values.
		# The clauses begin at lines[5].
		observed_frequency_list = self.count_p_clauses(lines[5:], self.hidden2, 1000)
		W = self.calculate_rejected_prob(p)
		P1, P2, P3 = self.calculate_add_probabilities(p, W)
		# Letting Q_i be the number of i-clauses (for this notion see `calculate_add_probabilities`),
		# it holds E[Q_i] = m * P_i, where P_i is calculated via `calculate_add_probabilities(p, W)`.
		exp_frequency_list = [m*P1, m*P2, m*P3]

		# We use Pearson's chi-squared test to determine whether there is a statistically significant difference between
		# the expected frequencies and the observed frequencies in the categories 1-correct, 2-correct, and 3-correct.
		_, p = chisquare(observed_frequency_list, exp_frequency_list)
		if p < 0.05:
			print("WARNING, WARNING. Self-destruction initialized.")
			print("p-value for the generated instance is", p)
		print("Everything's fine... probably. p-value for the generated instance is", p)

	def check_for_duplicate_clauses(self, lines):
		"""This method checks whether there are any duplicate clauses in `lines`.
		"""
		in_formula_set = set()
		for i in range(5, len(lines)):
			# We convert each clause in a list of strings.
			# The last entry "0" (signaling the end of the line/clause) will be removed.
			literals = lines[i].split()[:-1]
			lit_set = frozenset( [ int(s) for s in literals ] )
			# Ensures that the current clause has not been seen.
			self.assertNotIn(lit_set, in_formula_set)
			in_formula_set.add(lit_set)

	def test_unique_clauses(self):
		"""We test whether the generated formula contains only unique clauses.
		"""

		print(sys._getframe(  ).f_code.co_name)

		# We assume that there are no duplicate clauses. This tests checks that assumption.
		n = 20
		m = 1000
		s = 111
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m, s=s))
		g.main(n, m, s=s, o=o)
		lines = self.get_all_lines(path)
		self.check_for_duplicate_clauses(lines)

		# Let's do a second test.
		n = 100
		m = 10000
		s = 11
		k = 2
		o = tmpfolder
		path =  os.path.join(o, self.calculate_file_name(n, m, k=k, s=s))
		g.main(n, m, s=s, k=k, o=o)
		lines = self.get_all_lines(path)
		self.check_for_duplicate_clauses(lines)

if __name__ == 'main':
	unittest.main()


"""
WSGI config for codalab project.

This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.

Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.

"""
import os

# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "codalab.settings"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "codalab.settings")
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
os.environ.setdefault('DJANGO_LOCAL_CONFIGURATION',
                      'codalab.settings.local_environment')

# New Relic
import newrelic.agent
newrelic_ini_path = os.path.dirname(os.path.realpath(__file__))
if os.path.exists('%s/../newrelic.ini' % newrelic_ini_path):
    configuration_name = os.environ.get('DJANGO_CONFIGURATION')  # Dev, Prod, etc.
    os.environ.setdefault('NEW_RELIC_APP_NAME', 'Codalab - %s' % configuration_name)
    newrelic.agent.initialize('%s/../newrelic.ini' % newrelic_ini_path, configuration_name)

# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from configurations.wsgi import get_wsgi_application
application = get_wsgi_application()

# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)


import pytest
from day_3.house import *

@pytest.mark.parametrize("instructions, expected", [
    ('>', 2),
    ('^>v<', 4),
    ('^v^v^v^v^v', 2)
])
def test_one_present(instructions, expected):
    assert one_present(instructions) == expected

@pytest.mark.parametrize("instructions, expected", [
    ('^v', 3),
    ('^>v<', 3),
    ('^v^v^v^v^v', 11)
])
def test_santa_and_robot(instructions, expected):
    assert santa_and_robot(instructions) == expected