package com.atlassian.webdriver.bitbucket.util;

import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Test;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

public class DateUtilsTest {

    private static final ZonedDateTime BAD_DATE = LocalDate.of(2006, Month.FEBRUARY, 21)
            .atTime(2, 46, 13)
            .atZone(ZoneOffset.ofHours(-6));
    private static final ZonedDateTime DATE = LocalDate.of(2005, Month.JANUARY, 20)
            .atTime(1, 45, 12)
            .atZone(ZoneOffset.ofHours(10));
    private static final String DATE_TIMESTAMP = "2005-01-20T01:45:12+1000";
    private static final DateTimeFormatter PATTERN_DATE_TIME = DateTimeFormatter.ofPattern("dd MMM - hh:mm a");
    private static final DateTimeFormatter PATTERN_FULL = DateTimeFormatter.ofPattern("dd MMMM yyyy hh:mm a");
    private static final DateTimeFormatter PATTERN_LONG = DateTimeFormatter.ofPattern("dd MMMM yyyy");
    private static final DateTimeFormatter PATTERN_SHORT = DateTimeFormatter.ofPattern("dd MMM yyyy");
    private static final DateTimeFormatter PATTERN_TIME = DateTimeFormatter.ofPattern("hh:mm a");
    private static final DateTimeFormatter PATTERN_TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");

    @Test
    public void testParseServerTimestampParsesCorrectly() {
        ZonedDateTime actualDate = DateUtils.parseServerTimestamp(DATE_TIMESTAMP);

        assertThat(actualDate, equalTo(DATE));
    }

    @Test(expected = DateTimeException.class)
    public void testParseServerTimestampThrowsErrorOnInvalidTime() {
        DateUtils.parseServerTimestamp("iAmABadDate");
    }

    @Test
    public void testParseTestTimestampParsesCorrectly() {
        ZonedDateTime actualDate = DateUtils.parseTestTimestamp(DATE_TIMESTAMP);

        assertThat(actualDate, equalTo(DATE));
    }

    @Test(expected = DateTimeException.class)
    public void testParseTestTimestampThrowsErrorOnInvalidTime() {
        DateUtils.parseTestTimestamp("iAmABadDate");
    }

    @Test
    public void testDateTimeMatchesCorrectDates() {
        checkGoodMatch(DateUtils.DATE_TIME, PATTERN_DATE_TIME, 10);
    }

    @Test
    public void testDateTimeRejectsBadDates() {
        checkBadMatch(DateUtils.DATE_TIME, PATTERN_DATE_TIME);
    }

    @Test
    public void testDateTimeFormatsDateCorrectly() {
        checkFormat(DateUtils.DATE_TIME, PATTERN_DATE_TIME, 10);
    }

    @Test
    public void testFullMatchesCorrectDates() {
        checkGoodMatch(DateUtils.FULL, PATTERN_FULL, 9);
    }

    @Test
    public void testFullRejectsBadDates() {
        checkBadMatch(DateUtils.FULL, PATTERN_FULL);
    }

    @Test
    public void testFullFormatsDateCorrectly() {
        checkFormat(DateUtils.FULL, PATTERN_FULL, 9);
    }

    @Test
    public void testLongMatchesCorrectDates() {
        checkGoodMatch(DateUtils.LONG, PATTERN_LONG, 8);
    }

    @Test
    public void testLongRejectsBadDates() {
        checkBadMatch(DateUtils.LONG, PATTERN_LONG);
    }

    @Test
    public void testLongFormatsDatesCorrectly() {
        checkFormat(DateUtils.LONG, PATTERN_LONG, 8);
    }

    @Test
    public void testShortMatchesCorrectDates() {
        checkGoodMatch(DateUtils.SHORT, PATTERN_SHORT, 7);
    }

    @Test
    public void testShortRejectsBadDates() {
        checkBadMatch(DateUtils.SHORT, PATTERN_SHORT);
    }

    @Test
    public void testShortFormatsDatesCorrectly() {
        checkFormat(DateUtils.SHORT, PATTERN_SHORT, 7);
    }

    @Test
    public void testTimeMatchesCorrectDates() {
        checkGoodMatch(DateUtils.TIME_ONLY, PATTERN_TIME, 5);
    }

    @Test
    public void testTimeRejectsBadDates() {
        checkBadMatch(DateUtils.TIME_ONLY, PATTERN_TIME);
    }

    @Test
    public void testTimeFormatsTimeCorrectly() {
        checkFormat(DateUtils.TIME_ONLY, PATTERN_TIME, 5);
    }

    @Test
    public void testTimeStampMatchesGoodDates() {
        checkGoodInstantMatch(ZoneId.systemDefault());
        checkGoodInstantMatch(ZoneId.of("GMT+4"));
    }

    @Test
    public void testTimeStampMatchesRejectsBadDates() {
        Matcher<String> matcher = DateUtils.TIME_STAMP.matchesTimestamp(DATE_TIMESTAMP);

        assertThat(matcher.matches(PATTERN_TIMESTAMP.format(BAD_DATE)), equalTo(false));
        assertThat(matcher.matches("youCantParseThis"), equalTo(false));
    }

    @Test
    public void testTimeStampFormatIsANoop() {
        assertThat(DateUtils.TIME_STAMP.formatTimestamp(DATE_TIMESTAMP), equalTo(PATTERN_TIMESTAMP.format(DATE)));
    }

    private void checkBadMatch(DateUtils.FormatType formatType, DateTimeFormatter formatter) {
        Matcher<String> matcher = formatType.matchesTimestamp(DATE_TIMESTAMP);

        assertThat(matcher.matches("youCantParseThis"), equalTo(false));
        assertThat(matcher.matches(formatter.format(BAD_DATE)), equalTo(false));
    }

    private void checkFormat(DateUtils.FormatType formatType, DateTimeFormatter pattern, int offset) {
        checkFormat(formatType, pattern, ZoneId.systemDefault());
        checkFormat(formatType, pattern, ZoneOffset.ofHours(offset));
        checkFormat(formatType, pattern, ZoneOffset.ofHours(-offset));
    }

    private void checkFormat(DateUtils.FormatType formatType, DateTimeFormatter pattern, ZoneId zone) {
        String formatTimestamp = formatType.withTimeZone(zone).formatTimestamp(DATE_TIMESTAMP);
        assertThat(formatTimestamp, equalTo(pattern.withZone(zone).format(DATE)));
    }

    private void checkGoodInstantMatch(ZoneId id) {
        Matcher<String> matcher = DateUtils.TIME_STAMP.withTimeZone(id).matchesTimestamp(DATE_TIMESTAMP);

        String date = PATTERN_TIMESTAMP.format(DATE);
        assertThat(matcher.matches(date), Matchers.is(true));

        date = PATTERN_TIMESTAMP.format(DATE.withZoneSameInstant(ZoneOffset.ofHours(6)));
        assertThat(matcher.matches(date), Matchers.is(true));

        date = PATTERN_TIMESTAMP.format(DATE.withZoneSameInstant(ZoneOffset.ofHours(-5)));
        assertThat(matcher.matches(date), Matchers.is(true));
    }

    private void checkGoodMatch(DateUtils.FormatType formatType, DateTimeFormatter pattern, int offset) {
        checkGoodMatchInZone(formatType, pattern, ZoneId.systemDefault());
        checkGoodMatchInZone(formatType, pattern, ZoneOffset.ofHours(offset));
        checkGoodMatchInZone(formatType, pattern, ZoneOffset.ofHours(-offset));
    }

    private void checkGoodMatchInZone(DateUtils.FormatType formatType, DateTimeFormatter pattern, ZoneId zone) {
        Matcher<String> matcher = formatType.withTimeZone(zone).matchesTimestamp(DATE_TIMESTAMP);

        String goodDate = pattern.withZone(zone).format(DATE);

        assertThat(matcher.matches(goodDate), Matchers.is(true));
        assertThat(matcher.matches(goodDate.toLowerCase(Locale.ROOT)), Matchers.is(true));
        assertThat(matcher.matches(goodDate.toUpperCase(Locale.ROOT)), Matchers.is(true));
    }
}