package com.atlassian.stash.internal.web.timezone;

import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.timezone.TimeZoneService;
import com.atlassian.bitbucket.ui.ValidationErrors;
import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.atlassian.stash.internal.locale.InternalLocaleService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.io.IOException;
import java.time.ZoneId;
import java.util.*;

import static com.atlassian.stash.internal.web.soy.StashSoyViewResolver.SERVER_SOY_MODULE_KEY;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
import static org.mockito.Mockito.*;

public abstract class AbstractTimeZoneFormFragmentTest<T extends AbstractTimeZoneFormFragment> {

    private static final String FIELD_ZONE = "timezone";
    private static final String PARAM_DEFAULT_OPTION = "defaultOption";
    private static final String PARAM_ERRORS = "errors";
    private static final String PARAM_IS_LONG = "isLong";
    private static final String PARAM_REGIONS = "regions";
    private static final String PARAM_SELECTED = "selected";
    private static final String TIMEZONE_TEMPLATE = "bitbucket.internal.feature.timezone.timeZoneField";

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();
    @Spy
    SimpleI18nService i18nService = new SimpleI18nService(SimpleI18nService.Mode.RETURN_KEYS);
    @Mock
    SoyTemplateRenderer soyTemplateRenderer;
    @Mock
    InternalLocaleService localeService;
    @Mock
    TimeZoneService timeZoneService;
    @Mock
    private ValidationErrors validationErrors;

    T fragment;

    @Before
    public void before() {
        when(localeService.getLocale()).thenReturn(Locale.ENGLISH);
        when(timeZoneService.getDefaultTimeZone()).thenReturn(ZoneId.of("Australia/Sydney"));
        when(timeZoneService.getJvmTimeZone()).thenReturn(ZoneId.of("Australia/Sydney"));
        fragment = createFragment();
    }

    @Test
    public void testExecuteClearsServerTimeZone() {
        fragment.execute(Collections.emptyMap(), Collections.emptyMap());

        assertTimeZoneSet(null);
    }

    @Test
    public void testExecuteSetsServerTimeZone() {
        ZoneId zoneId = ZoneId.of("GMT+9");

        fragment.execute(parametersForZone(zoneId), Collections.emptyMap());
        assertTimeZoneSet(zoneId);
    }

    @Test
    public void testValidatePassesEmptyTimeZone() {
        fragment.validate(Collections.emptyMap(), validationErrors, Collections.emptyMap());

        verifyNoMoreInteractions(validationErrors);
    }

    @Test
    public void testValidateFailsWithInvalidTimeZone() {
        fragment.validate(parametersForZone("badTimeZone"), validationErrors, Collections.emptyMap());

        verify(validationErrors).addFieldError("timezone", i18nService.getMessage("bitbucket.web.timezone.invalid"));
    }

    @Test
    public void testValidatePassesWithValidTimeZone() {
        ZoneId id = ZoneId.of("GMT+10");
        fragment.validate(parametersForZone(id), validationErrors, Collections.emptyMap());

        verifyNoMoreInteractions(validationErrors);
    }

    @Test
    public void testViewRendersCurrentTimeZone() throws IOException {
        ZoneId zone = ZoneId.of("GMT-2");
        verifyTimeZoneRender(zone);
    }

    @Test
    public void testViewRendersNoTimeZone() throws IOException {
        verifyTimeZoneRender(null);
    }

    @Test
    public void testErrorRendersPassedTimeZoneWithErrors() {
        try {
            String zoneId = "badZone";
            String error = "error";
            StringBuilder appendable = new StringBuilder();

            fragment.doError(appendable, parametersForZone(zoneId), fieldError(error), Collections.emptyMap());

            verifyRenderedWith(appendable, zoneId, error);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testErrorRendersPassedTimeZoneWithNoErrors() {
        try {
            String zoneId = "goodZone";
            StringBuilder appendable = new StringBuilder();

            fragment.doError(appendable, parametersForZone(zoneId), Collections.emptyMap(), Collections.emptyMap());

            verifyRenderedWith(appendable, zoneId);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    abstract T createFragment();
    abstract void assertTimeZoneSet(ZoneId id);
    abstract void setTimeZone(ZoneId id);

    private static Map<String, Collection<String>> fieldError(String error) {
        ImmutableMultimap<String, String> errors = ImmutableMultimap.of(FIELD_ZONE, error);
        return errors.asMap();
    }

    private static Map<String, String[]> parametersForZone(String id) {
        return ImmutableMap.of(FIELD_ZONE, new String[]{id});
    }

    private static Map<String, String[]> parametersForZone(ZoneId zoneId) {
        return parametersForZone(zoneId.getId());
    }

    @SuppressWarnings("unchecked")
    private void verifyRenderedWith(StringBuilder appendable, String timeZone, String...errors) {
        ArgumentCaptor<Map> mapCaptor = ArgumentCaptor.forClass(Map.class);
        verify(soyTemplateRenderer).render(eq(appendable), eq(SERVER_SOY_MODULE_KEY), eq(TIMEZONE_TEMPLATE),
                mapCaptor.capture());

        Map<String, Object> map = mapCaptor.getValue();
        assertThat(map, hasEntry(equalTo(PARAM_DEFAULT_OPTION), equalTo(LocalizedZoneCollection.timeZone(
                ZoneId.of("Australia/Sydney"), Locale.ENGLISH, "bitbucket.web.timezone.default.time.zone", ""))));
        assertThat(map, hasEntry(equalTo(PARAM_REGIONS), equalTo(new LocalizedZoneCollection(localeService.getLocale()).getRegions())));
        assertThat(map, hasEntry(equalTo(PARAM_SELECTED), equalTo(timeZone)));
        assertThat(map, hasEntry(equalTo(PARAM_ERRORS), equalTo(ImmutableList.copyOf(errors))));
        assertThat(map, hasEntry(equalTo(PARAM_IS_LONG), equalTo(fragment.isLongField())));
    }

    private void verifyTimeZoneRender(ZoneId zoneId, String...errors) {
        setTimeZone(zoneId);

        StringBuilder builder = new StringBuilder();
        try {
            fragment.doView(builder, ImmutableMap.of());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        verifyRenderedWith(builder, Optional.ofNullable(zoneId).map(ZoneId::getId).orElse(""), errors);
    }
}
