"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
"""
import glob
import io
import json
import logging
import os

import pytest
import yaml
from core.label_config import parse_config, parse_config_to_json, validate_label_config
from django.test import TestCase
from projects.models import Project
from rest_framework.exceptions import ValidationError

from label_studio.tests.utils import make_annotation, make_prediction, make_task, project_id  # noqa

logger = logging.getLogger(__name__)


@pytest.mark.parametrize(
    'tasks_count, annotations_count, predictions_count',
    [
        [2, 2, 2],
    ],
)
@pytest.mark.django_db
def test_change_label_config_repeater(tasks_count, annotations_count, predictions_count, business_client, project_id):
    # Change label config to Repeater
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}">       <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
    # cr
    project = Project.objects.get(pk=project_id)
    for _ in range(0, tasks_count):
        task_id = make_task(
            {
                'data': {
                    'images': [
                        {'url': 'https://htx-pub.s3.amazonaws.com/demo/images/demo_stock_purchase_agreement/0001.jpg'},
                        {'url': 'https://htx-pub.s3.amazonaws.com/demo/images/demo_stock_purchase_agreement/0002.jpg'},
                        {'url': 'https://htx-pub.s3.amazonaws.com/demo/images/demo_stock_purchase_agreement/0003.jpg'},
                    ]
                }
            },
            project,
        ).id
        print('TASK_ID: %s' % task_id)
        for _ in range(0, annotations_count):
            print('COMPLETION')
            make_annotation(
                {
                    'result': [
                        {
                            'id': '_565WKjviN',
                            'type': 'rectanglelabels',
                            'value': {
                                'x': 21.451104100946377,
                                'y': 7.682926829268292,
                                'width': 54.73186119873817,
                                'height': 4.146341463414634,
                                'rotation': 0,
                                'rectanglelabels': ['Header'],
                            },
                            'origin': 'manual',
                            'to_name': 'page_0',
                            'from_name': 'labels_0',
                            'image_rotation': 0,
                            'original_width': 800,
                            'original_height': 1035,
                        }
                    ]
                },
                task_id,
            )

        for _ in range(0, predictions_count):
            make_prediction({'result': []}, task_id)

    # no changes - no errors
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200

    # delete unused labels
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200

    # delete used labels - 400
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 400


@pytest.mark.django_db
def test_parse_all_configs():
    folder_wildcard = './label_studio/annotation_templates'
    result = [y for x in os.walk(folder_wildcard) for y in glob.glob(os.path.join(x[0], '*.xml'))]
    for file in result:
        print(f'Parsing config: {file}')
        with open(file, mode='r') as f:
            config = f.read()
            assert parse_config(config)
            assert parse_config_to_json(config)
            validate_label_config(config)


@pytest.mark.django_db
def test_config_validation_for_choices_workaround(business_client, project_id):
    """
    Validate Choices tag for 1 choice with workaround
    Example bug DEV-3635
    """
    payload = {
        'label_config': '<View><Text value="$text" name="artist" /><View><Choices name="choices_1" toName="artist">'
        '<Choice name="choice_1" value="1"/></Choices></View><View>'
        '<Choices name="choices_2" toName="artist"><Choice name="choice_2" value="2"/></Choices>'
        '</View></View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200

    payload = {
        'label_config': '<View><Text value="$text" name="artist" /><View><Choices name="choices_1" toName="artist">'
        '<Choice name="choice_1" value="1"/></Choices><Choices name="choices_2" toName="artist">'
        '<Choice name="choice_2" value="2"/></Choices></View></View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200


@pytest.mark.django_db
def test_config_validation_for_missing_to_name_in_number_tag_fails(business_client, project_id):
    """
    Validate Number tag with missing to_name fails (see LEAP-245)
    """
    payload = {
        'label_config': (
            '<View>'
            '<Text name="question" value="$question" granularity="word"/>'
            '<Number name="number" to="question" required="true" />'
            '</View>'
        )
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 400
    response_data = response.json()
    assert "'toName' is a required property" in response_data['validation_errors']['label_config'][0]


@pytest.mark.django_db
def test_parse_wrong_xml(business_client, project_id):
    # Change label config to Repeater
    payload = {
        'label_config': '<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}">       <Label value="Header" hotkey="1"/> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.patch(
        f'/api/projects/{project_id}',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 200
    # Change label config to wrong XML
    payload = {
        'label_config': '1<View> <Repeater on="$images" indexFlag="{{idx}}"> <Image name="page_{{idx}}" value="$images" maxWidth="100%"/>     <Header value="Utterance Review"/>     <RectangleLabels name="labels_{{idx}}" toName="page_{{idx}}"> <Label value="Body" hotkey="2"/> <Label value="Footer" hotkey="3"/> </RectangleLabels> </Repeater> </View>'
    }
    response = business_client.post(
        f'/api/projects/{project_id}/validate',
        data=json.dumps(payload),
        content_type='application/json',
    )
    assert response.status_code == 400


@pytest.mark.django_db
def test_label_config_versions(business_client, project_id):
    with io.open(os.path.join(os.path.dirname(__file__), 'test_data/data_for_test_label_config_matrix.yml')) as f:
        test_suites = yaml.safe_load(f)
    for test_name, test_content in test_suites.items():
        payload = {'label_config': test_content['label_config']}
        response = business_client.post(
            f'/api/projects/{project_id}/validate',
            data=json.dumps(payload),
            content_type='application/json',
        )
        logger.warning(f'Test: {test_name}')
        assert response.status_code == test_content['status_code']


# Tag attribute validation: validate_label_config calls SDK _tag_attribute_validation
# and surfaces failures as ValidationError with descriptive messages.


class TestValidateLabelConfigTagAttributeValidation(TestCase):
    """
    validate_label_config() invokes the SDK's _tag_attribute_validation (per-tag
    attribute checks). These tests ensure that integration: when all tags pass
    validation we succeed; when any tag's validate_config() returns errors we
    get ValidationError with the error messages propagated.
    """

    # Configs used as concrete examples (currently Video is the only tag with validate_config).
    CONFIG_VALID_TAG_ATTRS = """
<View>
  <Video name="video" value="$video" framerate="25" defaultPlaybackSpeed="2" minPlaybackSpeed="0.5"/>
  <VideoRectangle name="box" toName="video" />
</View>
"""

    CONFIG_NO_TAG_SPECIFIC_ATTRS = """
<View>
  <Video name="video" value="$video"/>
  <VideoRectangle name="box" toName="video" />
</View>
"""

    CONFIG_TAG_ATTR_VALIDATION_FAILS_1 = """
<View>
  <Video name="video" value="$video" defaultPlaybackSpeed="15"/>
  <VideoRectangle name="box" toName="video" />
</View>
"""

    CONFIG_TAG_ATTR_VALIDATION_FAILS_2 = """
<View>
  <Video name="video" value="$video" defaultPlaybackSpeed="5" minPlaybackSpeed="10"/>
  <VideoRectangle name="box" toName="video" />
</View>
"""

    def test_tag_attribute_validation_passes_when_all_tags_valid(self):
        """
        When every object tag's validate_config() returns no errors,
        validate_label_config completes without raising.
        """
        validate_label_config(self.CONFIG_VALID_TAG_ATTRS)

    def test_tag_attribute_validation_passes_when_tags_have_no_attrs_to_validate(self):
        """
        When config has object tags that do not define attribute validation
        (or have no attributes to validate), validate_label_config completes without raising.
        """
        validate_label_config(self.CONFIG_NO_TAG_SPECIFIC_ATTRS)

    def test_tag_attribute_validation_failure_raises_with_descriptive_message(self):
        """
        When a tag's validate_config() returns errors, validate_label_config
        raises ValidationError and the exception message includes the tag identifier
        and the validation failure detail (so _tag_attribute_validation is invoked and
        its errors are propagated).
        """
        with self.assertRaises(ValidationError) as ctx:
            validate_label_config(self.CONFIG_TAG_ATTR_VALIDATION_FAILS_1)
        msg = str(ctx.exception)
        assert 'video' in msg.lower()
        assert '15' in msg

    def test_tag_attribute_validation_failure_raises_with_cross_attr_message(self):
        """
        When a tag's validate_config() returns errors (e.g. cross-attribute constraint),
        validate_label_config raises ValidationError and the message describes the constraint.
        """
        with self.assertRaises(ValidationError) as ctx:
            validate_label_config(self.CONFIG_TAG_ATTR_VALIDATION_FAILS_2)
        msg = str(ctx.exception)
        assert 'video' in msg.lower()
        assert 'must not exceed' in msg or 'exceed' in msg
