/**
 * Copyright 2011-2019 Asakusa Framework Team.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.asakusafw.testdriver.excel;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.Calendar;

import org.junit.Test;

import com.asakusafw.testdriver.core.DataModelDefinition;
import com.asakusafw.testdriver.core.DataModelReflection;
import com.asakusafw.testdriver.core.SpiVerifyRuleProvider;
import com.asakusafw.testdriver.core.TestContext;
import com.asakusafw.testdriver.core.VerifyContext;
import com.asakusafw.testdriver.core.VerifyRule;
import com.asakusafw.testdriver.core.VerifyRuleProvider;
import com.asakusafw.testdriver.model.SimpleDataModelDefinition;
import com.asakusafw.testdriver.rule.DataModelCondition;

/**
 * Test for {@link ExcelSheetRuleProvider}.
 * @since 0.2.0
 */
public class ExcelSheetRuleProviderTest {

    static final DataModelDefinition<Simple> SIMPLE = new SimpleDataModelDefinition<>(Simple.class);

    /**
     * simple verification.
     * @throws Exception if occur
     */
    @Test
    public void simple() throws Exception {
        ExcelSheetRuleProvider provider = new ExcelSheetRuleProvider();
        VerifyRule rule = provider.get(SIMPLE, context(10), uri("verify/simple.xls", ":0"));
        assertThat(rule, not(nullValue()));

        assertThat(rule.getKey(obj(100, "a")), equalTo(rule.getKey(obj(100, "b"))));
        assertThat(rule.getKey(obj(100, "a")), not(equalTo(rule.getKey(obj(200, "a")))));

        assertThat(rule.verify(obj(1, "a"), obj(2, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
    }

    /**
     * integration test with test-data-generator.
     * @throws Exception if occur
     */
    @Test
    public void integration() throws Exception {
        ExcelSheetRuleProvider provider = new ExcelSheetRuleProvider();
        VerifyRule rule = provider.get(SIMPLE, context(10), uri("it/simple.xls", ":2"));
        assertThat(rule, not(nullValue()));

        assertThat(rule.getKey(obj(100, "a")), equalTo(rule.getKey(obj(100, "b"))));
        assertThat(rule.getKey(obj(100, "a")), not(equalTo(rule.getKey(obj(200, "a")))));

        assertThat(rule.verify(obj(1, "a"), obj(2, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
    }

    /**
     * simple verification via SPI.
     * @throws Exception if occur
     */
    @Test
    public void spi() throws Exception {
        VerifyRuleProvider provider = new SpiVerifyRuleProvider(ExcelSheetRuleProvider.class.getClassLoader());
        VerifyRule rule = provider.get(SIMPLE, context(10), uri("verify/simple.xls", ":0"));
        assertThat(rule, not(nullValue()));

        assertThat(rule.getKey(obj(100, "a")), equalTo(rule.getKey(obj(100, "b"))));
        assertThat(rule.getKey(obj(100, "a")), not(equalTo(rule.getKey(obj(200, "a")))));

        assertThat(rule.verify(obj(1, "a"), obj(2, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
    }

    /**
     * {@link DataModelCondition} - strict.
     * @throws Exception if occur
     */
    @Test
    public void strict() throws Exception {
        VerifyRule rule = rule("verify/strict.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
        assertThat(rule.verify(null, obj(1, "a")), not(nullValue()));
        assertThat(rule.verify(obj(1, "a"), null), not(nullValue()));
    }

    /**
     * {@link DataModelCondition} - ignore absent.
     * @throws Exception if occur
     */
    @Test
    public void ignore_absent() throws Exception {
        VerifyRule rule = rule("verify/ignore_absent.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
        assertThat(rule.verify(null, obj(1, "a")), not(nullValue()));
        assertThat(rule.verify(obj(1, "a"), null), is(nullValue()));
    }

    /**
     * {@link DataModelCondition} - ignore unexpected.
     * @throws Exception if occur
     */
    @Test
    public void ignore_unexpected() throws Exception {
        VerifyRule rule = rule("verify/ignore_unexpected.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
        assertThat(rule.verify(null, obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), null), not(nullValue()));
    }

    /**
     * {@link DataModelCondition} - intersect.
     * @throws Exception if occur
     */
    @Test
    public void intersect() throws Exception {
        VerifyRule rule = rule("verify/intersect.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
        assertThat(rule.verify(null, obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), null), is(nullValue()));
    }

    /**
     * {@link DataModelCondition} - ignore all.
     * @throws Exception if occur
     */
    @Test
    public void skip() throws Exception {
        VerifyRule rule = rule("verify/skip.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), is(nullValue()));
        assertThat(rule.verify(null, obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), null), is(nullValue()));
    }

    /**
     * {@link DataModelCondition} - ignore all.
     * @throws Exception if occur
     */
    @Test
    public void skip_key() throws Exception {
        VerifyRule rule = rule("verify/skip.xls");
        Object k0 = rule.getKey(obj(1, "a"));
        Object k1 = rule.getKey(obj(1, "a"));
        Object k2 = rule.getKey(obj(1, "a"));
        assertThat(k0, not(equalTo(k1)));
        assertThat(k0, not(equalTo(k2)));
        assertThat(k1, not(equalTo(k2)));
    }

    /**
     * undefined properties.
     * @throws Exception if occur
     */
    @Test(expected = IOException.class)
    public void name_unknown() throws Exception {
        rule("verify/name_unknown.xls");
    }

    /**
     * empty property names.
     * @throws Exception if occur
     */
    @Test
    public void name_empty() throws Exception {
        VerifyRule rule = rule("verify/name_empty.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(2, "b")), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - don't care.
     * @throws Exception if occur
     */
    @Test
    public void value_any() throws Exception {
        VerifyRule rule = rule("verify/value_any.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(2, "b")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(null, null)), is(nullValue()));
        assertThat(rule.verify(null, obj(1, "a")), not(nullValue()));
        assertThat(rule.verify(obj(1, "a"), null), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - keys.
     * @throws Exception if occur
     */
    @Test
    public void value_keys() throws Exception {
        VerifyRule rule = rule("verify/value_keys.xls");
        assertThat(rule.getKey(obj(1, "a")), equalTo(rule.getKey(obj(1, "a"))));
        assertThat(rule.getKey(obj(2, "b")), equalTo(rule.getKey(obj(2, "b"))));
        assertThat(rule.getKey(obj(1, "a")), not(equalTo(rule.getKey(obj(2, "a")))));
        assertThat(rule.getKey(obj(1, "a")), not(equalTo(rule.getKey(obj(1, "b")))));

        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(2, "b")), is(nullValue()));
        assertThat(rule.verify(null, obj(1, "a")), not(nullValue()));
        assertThat(rule.verify(obj(1, "a"), null), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - equals.
     * @throws Exception if occur
     */
    @Test
    public void value_equal() throws Exception {
        VerifyRule rule = rule("verify/value_equal.xls");
        assertThat(rule.verify(obj(1, "a"), obj(1, "a")), is(nullValue()));
        assertThat(rule.verify(obj(2, "b"), obj(2, "b")), is(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(1, "b")), not(nullValue()));
        assertThat(rule.verify(obj(1, "a"), obj(2, "a")), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - contain.
     * @throws Exception if occur
     */
    @Test
    public void value_contain() throws Exception {
        VerifyRule rule = rule("verify/value_contain.xls");
        assertThat(rule.verify(obj(0, "ab"), obj(0, "ab")), is(nullValue()));
        assertThat(rule.verify(obj(0, "ab"), obj(0, "aba")), is(nullValue()));
        assertThat(rule.verify(obj(0, "ab"), obj(0, "aca")), not(nullValue()));
        assertThat(rule.verify(obj(0, "ab"), obj(0, "a")), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - contain for not a string property.
     * @throws Exception if occur
     */
    @Test(expected = IOException.class)
    public void value_contain_error() throws Exception {
        rule("verify/value_contain_error.xls");
    }

    /**
     * {@link ValueConditionKind} - today.
     * @throws Exception if occur
     */
    @Test
    public void value_today() throws Exception {
        // 2011/03/31 23:00:00 -> 23:30:00
        VerifyContext context = context(30);
        VerifyRule rule = rule("verify/value_today.xls", context);

        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(2011, 2, 30);
        assertThat(rule.verify(obj(0, ""), date(calendar)), not(nullValue()));
        calendar.set(2011, 2, 31);
        assertThat(rule.verify(obj(0, ""), date(calendar)), is(nullValue()));
        calendar.set(2011, 3, 1);
        assertThat(rule.verify(obj(0, ""), date(calendar)), not(nullValue()));
        calendar.set(2011, 3, 2);
        assertThat(rule.verify(obj(0, ""), date(calendar)), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - today (but test was started yesterday).
     * @throws Exception if occur
     */
    @Test
    public void value_today_started_yesterday() throws Exception {
        // 2011/03/31 23:00:00 -> 2011/04/01 0:30:00
        VerifyContext context = context(90);
        VerifyRule rule = rule("verify/value_today.xls", context);

        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(2011, 2, 30);
        assertThat(rule.verify(obj(0, ""), date(calendar)), not(nullValue()));
        calendar.set(2011, 2, 31);
        assertThat(rule.verify(obj(0, ""), date(calendar)), is(nullValue()));
        calendar.set(2011, 3, 1);
        assertThat(rule.verify(obj(0, ""), date(calendar)), is(nullValue()));
        calendar.set(2011, 3, 2);
        assertThat(rule.verify(obj(0, ""), date(calendar)), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - today for not a date/datetime property.
     * @throws Exception if occur
     */
    @Test(expected = IOException.class)
    public void value_today_error() throws Exception {
        rule("verify/value_today_error.xls");
    }

    /**
     * {@link ValueConditionKind} - now.
     * @throws Exception if occur
     */
    @Test
    public void value_now() throws Exception {
        // 2011/03/31 23:00:00 -> 23:30:00
        VerifyContext context = context(30);
        VerifyRule rule = rule("verify/value_now.xls", context);

        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(2011, 2, 31, 0, 0, 0);
        assertThat(rule.verify(obj(0, ""), datetime(calendar)), not(nullValue()));
        calendar.set(2011, 2, 31, 22, 59, 59);
        assertThat(rule.verify(obj(0, ""), datetime(calendar)), not(nullValue()));
        calendar.set(2011, 2, 31, 23,  0,  0);
        assertThat(rule.verify(obj(0, ""), datetime(calendar)), is(nullValue()));
        calendar.set(2011, 2, 31, 23, 15,  0);
        assertThat(rule.verify(obj(0, ""), datetime(calendar)), is(nullValue()));
        calendar.set(2011, 2, 31, 23, 30, 00);
        assertThat(rule.verify(obj(0, ""), datetime(calendar)), is(nullValue()));
        calendar.set(2011, 2, 31, 23, 30, 01);
        assertThat(rule.verify(obj(0, ""), datetime(calendar)), not(nullValue()));
        calendar.set(2011, 2, 31, 23, 45, 00);
        assertThat(rule.verify(obj(0, ""), datetime(calendar)), not(nullValue()));
    }

    /**
     * {@link ValueConditionKind} - now for not a datetime property.
     * @throws Exception if occur
     */
    @Test(expected = IOException.class)
    public void value_now_error() throws Exception {
        rule("verify/value_now_error.xls");
    }

    /**
     * {@link ValueConditionKind} - expr.
     * @throws Exception if occur
     */
    @Test
    public void value_expr() throws Exception {
        ExcelSheetRuleProvider provider = new ExcelSheetRuleProvider();
        VerifyRule rule = provider.get(SIMPLE, context(10), uri("verify/value_approx.xls", ":0"));
        assertThat(rule, not(nullValue()));

        assertThat(rule.verify(num(1, 1.0), num(1, 1.0)), is(nullValue()));
        assertThat(rule.verify(num(1, 1.0), num(1, 1.49)), is(nullValue()));
        assertThat(rule.verify(num(1, 1.0), num(1, 1.51)), is(notNullValue()));
        assertThat(rule.verify(num(1, 1.0), num(1, 0.51)), is(nullValue()));
        assertThat(rule.verify(num(1, 1.0), num(1, 0.49)), is(notNullValue()));
    }

    /**
     * {@link ValueConditionKind} - expr.
     * @throws Exception if occur
     */
    @Test(expected = IOException.class)
    public void value_expr_unsupported() throws Exception {
        rule("verify/value_expr_unsupported.xls");
    }

    /**
     * {@link ValueConditionKind} - expr.
     * @throws Exception if occur
     */
    @Test(expected = IOException.class)
    public void value_expr_invalid() throws Exception {
        rule("verify/value_approx_error.xls");
    }

    /**
     * {@link NullityConditionKind} - normal checking.
     * @throws Exception if occur
     */
    @Test
    public void nullity_normal() throws Exception {
        VerifyRule rule = rule("verify/nullity_normal.xls");
        assertThat(rule.verify(obj(0, "a"), obj(0, "a")), is(nullValue()));
        assertThat(rule.verify(obj(null, "a"), obj(null, "a")), is(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(0, null)), is(nullValue()));
        assertThat(rule.verify(obj(0, "a"), obj(null, "a")), not(nullValue()));
        assertThat(rule.verify(obj(0, "a"), obj(0, null)), is(nullValue()));
    }

    /**
     * {@link NullityConditionKind} - accept absent.
     * @throws Exception if occur
     */
    @Test
    public void nullity_AA() throws Exception {
        VerifyRule rule = rule("verify/nullity_AA.xls");
        assertThat(rule.verify(obj(0, "a"), obj(0, "a")), not(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(0, "a")), not(nullValue()));
        assertThat(rule.verify(obj(0, "a"), obj(0, null)), is(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(0, null)), is(nullValue()));
    }

    /**
     * {@link NullityConditionKind} - accept present.
     * @throws Exception if occur
     */
    @Test
    public void nullity_AP() throws Exception {
        VerifyRule rule = rule("verify/nullity_AP.xls");
        assertThat(rule.verify(obj(0, "a"), obj(0, "a")), is(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(0, "a")), is(nullValue()));
        assertThat(rule.verify(obj(0, "a"), obj(0, null)), not(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(0, null)), not(nullValue()));
    }

    /**
     * {@link NullityConditionKind} - deny present.
     * @throws Exception if occur
     */
    @Test
    public void nullity_DP() throws Exception {
        VerifyRule rule = rule("verify/nullity_DP.xls");
        assertThat(rule.verify(obj(0, "a"), obj(null, "a")), not(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(null, "a")), not(nullValue()));
        assertThat(rule.verify(obj(0, "a"), obj(null, null)), is(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(null, null)), is(nullValue()));

        assertThat(rule.verify(obj(0, null), obj(0, null)), not(nullValue()));
    }

    /**
     * {@link NullityConditionKind} - deny absent.
     * @throws Exception if occur
     */
    @Test
    public void nullity_DA() throws Exception {
        VerifyRule rule = rule("verify/nullity_DA.xls");
        assertThat(rule.verify(obj(0, "a"), obj(0, "a")), is(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(0, "a")), is(nullValue()));
        assertThat(rule.verify(obj(0, "a"), obj(0, null)), not(nullValue()));
        assertThat(rule.verify(obj(0, null), obj(0, null)), not(nullValue()));

        assertThat(rule.verify(obj(null, "a"), obj(null, "a")), not(nullValue()));
    }

    private DataModelReflection obj(Integer number, String text) {
        Simple simple = new Simple();
        simple.number = number;
        simple.text = text;
        return SIMPLE.toReflection(simple);
    }

    private DataModelReflection date(Calendar date) {
        Simple simple = new Simple();
        simple.dateValue = date;
        return SIMPLE.toReflection(simple);
    }

    private DataModelReflection datetime(Calendar dateTime) {
        Simple simple = new Simple();
        simple.datetimeValue = dateTime;
        return SIMPLE.toReflection(simple);
    }

    private DataModelReflection num(Integer number, double value) {
        Simple simple = new Simple();
        simple.number = number;
        simple.doubleValue = value;
        return SIMPLE.toReflection(simple);
    }

    private VerifyContext context(int elapsedMinutes) {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(2011, 2, 31, 23, 0, 0);
        VerifyContext result = new VerifyContext(new TestContext.Empty(), calendar.getTime());

        calendar.add(Calendar.MINUTE, elapsedMinutes);
        result.setTestFinished(calendar.getTime());
        return result;
    }

    private URI uri(String file, String fragment) throws Exception {
        URL url = getClass().getResource(file);
        assertThat(file, url, not(nullValue()));
        URI resource = url.toURI();
        URI uri = new URI(
                resource.getScheme(),
                resource.getUserInfo(),
                resource.getHost(),
                resource.getPort(),
                resource.getPath(),
                resource.getQuery(),
                fragment);
        return uri;
    }

    private VerifyRule rule(String name) throws Exception {
        return rule(name, context(10));
    }

    private VerifyRule rule(String name, VerifyContext context) throws Exception {
        assert name != null;
        ExcelSheetRuleProvider provider = new ExcelSheetRuleProvider();
        VerifyRule rule = provider.get(SIMPLE, context, uri(name, ":0"));
        assertThat(rule, not(nullValue()));
        return rule;
    }
}
