/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.kafka.streams.processor.internals;

import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.kstream.EmitStrategy;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.SlidingWindows;
import org.apache.kafka.streams.kstream.internals.InternalNameProvider;
import org.apache.kafka.streams.kstream.internals.MaterializedInternal;
import org.apache.kafka.streams.kstream.internals.SlidingWindowStoreMaterializer;
import org.apache.kafka.streams.processor.StateStore;
import org.apache.kafka.streams.state.BuiltInDslStoreSuppliers;
import org.apache.kafka.streams.state.DslStoreSuppliers;
import org.apache.kafka.streams.state.HeadersBytesStoreSupplier;
import org.apache.kafka.streams.state.TimestampedWindowStore;
import org.apache.kafka.streams.state.TimestampedWindowStoreWithHeaders;
import org.apache.kafka.streams.state.WindowBytesStoreSupplier;
import org.apache.kafka.streams.state.WindowStore;
import org.apache.kafka.streams.state.internals.CachingWindowStore;
import org.apache.kafka.streams.state.internals.ChangeLoggingTimestampedWindowBytesStore;
import org.apache.kafka.streams.state.internals.ChangeLoggingTimestampedWindowBytesStoreWithHeaders;
import org.apache.kafka.streams.state.internals.InMemoryWindowStore;
import org.apache.kafka.streams.state.internals.MeteredTimestampedWindowStore;
import org.apache.kafka.streams.state.internals.MeteredTimestampedWindowStoreWithHeaders;
import org.apache.kafka.streams.state.internals.WrappedStateStore;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import java.time.Duration;

import static java.util.Collections.emptyMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.STRICT_STUBS)
public class SlidingWindowStoreMaterializerTest {

    private static final String STORE_PREFIX = "prefix";
    private static final String STORE_NAME = "name";
    private static final long TIME_DIFFERENCE_MS = 5000L;

    @Mock
    private InternalNameProvider nameProvider;
    @Mock
    private StreamsConfig streamsConfig;

    private final WindowStore<Bytes, byte[]> innerWindowStore =
        new InMemoryWindowStore(STORE_NAME, 60000L, TIME_DIFFERENCE_MS, false, "metricScope");

    private SlidingWindows windows;
    private EmitStrategy emitStrategy;

    @BeforeEach
    public void setUp() {
        windows = SlidingWindows.ofTimeDifferenceWithNoGrace(Duration.ofMillis(TIME_DIFFERENCE_MS));
        emitStrategy = EmitStrategy.onWindowUpdate();

        doReturn(emptyMap())
            .when(streamsConfig).originals();
        doReturn(new BuiltInDslStoreSuppliers.RocksDBDslStoreSuppliers())
                .when(streamsConfig).getConfiguredInstance(
                    StreamsConfig.DSL_STORE_SUPPLIERS_CLASS_CONFIG,
                    DslStoreSuppliers.class,
                    emptyMap()
            );
        lenient().doReturn("timestamped")
                .when(streamsConfig).getString(StreamsConfig.DSL_STORE_FORMAT_CONFIG);
    }

    private final class HeadersStoreSupplier implements WindowBytesStoreSupplier, HeadersBytesStoreSupplier {
        @Override
        public String name() {
            return STORE_NAME;
        }

        @Override
        public WindowStore<Bytes, byte[]> get() {
            return innerWindowStore;
        }

        @Override
        public String metricsScope() {
            return "metricScope";
        }

        @Override
        public long segmentIntervalMs() {
            return 0;
        }

        @Override
        public long windowSize() {
            return 0;
        }

        @Override
        public boolean retainDuplicates() {
            return false;
        }

        @Override
        public long retentionPeriod() {
            return 0;
        }
    }

    @Test
    public void shouldCreateTimestampedBuilderWithCachingAndLoggingEnabledByDefault() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.as("store"), nameProvider, STORE_PREFIX);

        final TimestampedWindowStore<String, String> store = getTimestampedStore(materialized);
        final WrappedStateStore<?, ?, ?> caching = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        final StateStore logging = caching.wrapped();

        assertInstanceOf(MeteredTimestampedWindowStore.class, store);
        assertInstanceOf(CachingWindowStore.class, caching);
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStore.class, logging);
    }

    @Test
    public void shouldCreateTimestampedBuilderWithCachingDisabled() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized = new MaterializedInternal<>(
            Materialized.<String, String, WindowStore<Bytes, byte[]>>as("store").withCachingDisabled(), nameProvider, STORE_PREFIX
        );

        final TimestampedWindowStore<String, String> store = getTimestampedStore(materialized);

        final WrappedStateStore<?, ?, ?> logging = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStore.class, store);
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStore.class, logging);
    }

    @Test
    public void shouldCreateTimestampedBuilderWithLoggingDisabled() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized = new MaterializedInternal<>(
            Materialized.<String, String, WindowStore<Bytes, byte[]>>as("store").withLoggingDisabled(), nameProvider, STORE_PREFIX
        );

        final TimestampedWindowStore<String, String> store = getTimestampedStore(materialized);

        final WrappedStateStore<?, ?, ?> caching = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStore.class, store);
        assertInstanceOf(CachingWindowStore.class, caching);
        assertFalse(caching.wrapped() instanceof ChangeLoggingTimestampedWindowBytesStore);
    }

    @Test
    public void shouldCreateTimestampedBuilderWithCachingAndLoggingDisabled() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized = new MaterializedInternal<>(
            Materialized.<String, String, WindowStore<Bytes, byte[]>>as("store").withCachingDisabled().withLoggingDisabled(), nameProvider, STORE_PREFIX
        );

        final TimestampedWindowStore<String, String> store = getTimestampedStore(materialized);

        final StateStore wrapped = ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStore.class, store);
        assertFalse(wrapped instanceof CachingWindowStore);
        assertFalse(wrapped instanceof ChangeLoggingTimestampedWindowBytesStore);
    }

    @Test
    public void shouldCreateHeadersStoreWithProvidedSupplierAndCachingAndLoggingEnabledByDefault() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.as(new HeadersStoreSupplier()), nameProvider, STORE_PREFIX);

        final TimestampedWindowStoreWithHeaders<String, String> store = getHeadersStore(materialized);

        final WrappedStateStore<?, ?, ?> caching = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        final StateStore logging = caching.wrapped();
        assertEquals(innerWindowStore.name(), store.name());
        assertInstanceOf(MeteredTimestampedWindowStoreWithHeaders.class, store);
        assertInstanceOf(CachingWindowStore.class, caching);
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStoreWithHeaders.class, logging);
    }

    @Test
    public void shouldCreateHeadersStoreWithProvidedSupplierAndCachingDisabled() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.<String, String>as(new HeadersStoreSupplier()).withCachingDisabled(), nameProvider, STORE_PREFIX);

        final TimestampedWindowStoreWithHeaders<String, String> store = getHeadersStore(materialized);

        final WrappedStateStore<?, ?, ?> logging = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStoreWithHeaders.class, store);
        assertEquals(innerWindowStore.name(), store.name());
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStoreWithHeaders.class, logging);
    }

    @Test
    public void shouldCreateHeadersStoreWithProvidedSupplierAndLoggingDisabled() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.<String, String>as(new HeadersStoreSupplier()).withLoggingDisabled(), nameProvider, STORE_PREFIX);

        final TimestampedWindowStoreWithHeaders<String, String> store = getHeadersStore(materialized);

        final WrappedStateStore<?, ?, ?> caching = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertEquals(innerWindowStore.name(), store.name());
        assertInstanceOf(MeteredTimestampedWindowStoreWithHeaders.class, store);
        assertInstanceOf(CachingWindowStore.class, caching);
        assertFalse(caching.wrapped() instanceof ChangeLoggingTimestampedWindowBytesStoreWithHeaders);
    }

    @Test
    public void shouldCreateHeadersStoreWithProvidedSupplierAndCachingAndLoggingDisabled() {
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.<String, String>as(new HeadersStoreSupplier()).withCachingDisabled().withLoggingDisabled(), nameProvider, STORE_PREFIX);

        final TimestampedWindowStoreWithHeaders<String, String> store = getHeadersStore(materialized);

        final StateStore wrapped = ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertEquals(innerWindowStore.name(), store.name());
        assertInstanceOf(MeteredTimestampedWindowStoreWithHeaders.class, store);
        assertFalse(wrapped instanceof CachingWindowStore);
        assertFalse(wrapped instanceof ChangeLoggingTimestampedWindowBytesStoreWithHeaders);
    }

    @Test
    public void shouldCreateTimestampedStoreWithOnWindowCloseByDefault() {
        emitStrategy = EmitStrategy.onWindowClose();

        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.<String, String, WindowStore<Bytes, byte[]>>as("store")
                .withCachingDisabled(), nameProvider, STORE_PREFIX);

        final TimestampedWindowStore<String, String> store = getTimestampedStore(materialized);

        final WrappedStateStore<?, ?, ?> logging = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStore.class, store);
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStore.class, logging);
    }

    @Test
    public void shouldCreateTimestampedStoreWithOnWindowCloseAndCachingDisabled() {
        emitStrategy = EmitStrategy.onWindowClose();

        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.as("store"), nameProvider, STORE_PREFIX);

        final TimestampedWindowStore<String, String> store = getTimestampedStore(materialized);

        final WrappedStateStore<?, ?, ?> logging = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStore.class, store);
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStore.class, logging);
    }

    @Test
    public void shouldCreateHeadersStoreWithProvidedSupplierOnWindowClose() {
        emitStrategy = EmitStrategy.onWindowClose();

        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.<String, String>as(new HeadersStoreSupplier())
                .withCachingDisabled(), nameProvider, STORE_PREFIX);

        final TimestampedWindowStoreWithHeaders<String, String> store = getHeadersStore(materialized);

        final WrappedStateStore<?, ?, ?> logging = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStoreWithHeaders.class, store);
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStoreWithHeaders.class, logging);
    }

    @Test
    public void shouldCreateHeadersStoreWithProvidedSupplierOnWindowCloseAndCachingDisabled() {
        emitStrategy = EmitStrategy.onWindowClose();

        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized =
            new MaterializedInternal<>(Materialized.as(new HeadersStoreSupplier()),
                nameProvider, STORE_PREFIX);

        final TimestampedWindowStoreWithHeaders<String, String> store = getHeadersStore(materialized);

        final WrappedStateStore<?, ?, ?> logging = (WrappedStateStore<?, ?, ?>) ((WrappedStateStore<?, ?, ?>) store).wrapped();
        assertInstanceOf(MeteredTimestampedWindowStoreWithHeaders.class, store);
        assertInstanceOf(ChangeLoggingTimestampedWindowBytesStoreWithHeaders.class, logging);
    }

    @SuppressWarnings("unchecked")
    private TimestampedWindowStore<String, String> getTimestampedStore(
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized) {
        final SlidingWindowStoreMaterializer<String, String> materializer =
            new SlidingWindowStoreMaterializer<>(materialized, windows, emitStrategy);
        materializer.configure(streamsConfig);
        return (TimestampedWindowStore<String, String>) materializer.builder().build();
    }

    @SuppressWarnings("unchecked")
    private TimestampedWindowStoreWithHeaders<String, String> getHeadersStore(
        final MaterializedInternal<String, String, WindowStore<Bytes, byte[]>> materialized
    ) {
        final SlidingWindowStoreMaterializer<String, String> materializer =
            new SlidingWindowStoreMaterializer<>(materialized, windows, emitStrategy);
        materializer.configure(streamsConfig);
        return (TimestampedWindowStoreWithHeaders<String, String>) materializer.builder().build();
    }
}
