/**
 * Copyright 2016-2020 The OpenTracing Authors
 *
 * 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 io.opentracing.contrib.spring.web.client;

import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.mock.MockSpan;
import io.opentracing.mock.MockTracer;
import io.opentracing.tag.Tags;
import io.opentracing.util.ThreadLocalScopeManager;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.http.ResponseEntity;

import java.net.UnknownHostException;
import java.util.List;
import java.util.function.Function;

import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;

/**
 * @author Pavol Loffay
 */
public abstract class AbstractTracingClientTest {

    protected interface Client {
        <T> ResponseEntity<T> getForEntity(String url, Class<T> type);
    }

    protected final MockTracer mockTracer = new MockTracer(new ThreadLocalScopeManager(),
            MockTracer.Propagator.TEXT_MAP);
    protected final Client client;
    protected final String componentName;

    protected AbstractTracingClientTest(final Function<Tracer, Client> clientFactory, final String componentName) {
        this.client = clientFactory.apply(mockTracer);
        this.componentName = componentName;
    }

    @Rule
    public final WireMockRule wireMockRule = new WireMockRule(wireMockConfig()
            .dynamicPort()
            .extensions(new ResponseTemplateTransformer(true)));

    @Before
    public void before() {
        mockTracer.reset();
    }

    @Test
    public void testStandardTags() {
        final String path = "/foo";
        final String url = wireMockRule.url(path);
        {
            stubFor(get(urlPathEqualTo(path))
                    .willReturn(ok()));
            client.getForEntity(url, String.class);
        }

        List<MockSpan> mockSpans = mockTracer.finishedSpans();
        Assert.assertEquals(1, mockSpans.size());

        MockSpan mockSpan = mockSpans.get(0);
        Assert.assertEquals("GET", mockSpan.operationName());
        Assert.assertEquals(6, mockSpan.tags().size());
        Assert.assertEquals(componentName, mockSpan.tags().get(Tags.COMPONENT.getKey()));
        Assert.assertEquals(Tags.SPAN_KIND_CLIENT, mockSpan.tags().get(Tags.SPAN_KIND.getKey()));
        Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey()));
        Assert.assertEquals(url, mockSpan.tags().get(Tags.HTTP_URL.getKey()));
        Assert.assertEquals(200, mockSpan.tags().get(Tags.HTTP_STATUS.getKey()));
        Assert.assertEquals(wireMockRule.port(), mockSpan.tags().get(Tags.PEER_PORT.getKey()));
        Assert.assertEquals(0, mockSpan.logEntries().size());
    }

    @Test
    public void testStandardTagsWithPort() {
        final String path = "/foo";
        final String url = wireMockRule.url(path);
        {
            stubFor(get(urlPathEqualTo(path))
                    .willReturn(ok()));
            client.getForEntity(url, String.class);
        }

        List<MockSpan> mockSpans = mockTracer.finishedSpans();
        Assert.assertEquals(1, mockSpans.size());

        MockSpan mockSpan = mockSpans.get(0);
        Assert.assertEquals("GET", mockSpan.operationName());
        Assert.assertEquals(6, mockSpan.tags().size());
        Assert.assertEquals(componentName, mockSpan.tags().get(Tags.COMPONENT.getKey()));
        Assert.assertEquals(Tags.SPAN_KIND_CLIENT, mockSpan.tags().get(Tags.SPAN_KIND.getKey()));
        Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey()));
        Assert.assertEquals(url, mockSpan.tags().get(Tags.HTTP_URL.getKey()));
        Assert.assertEquals(200, mockSpan.tags().get(Tags.HTTP_STATUS.getKey()));
        Assert.assertEquals(wireMockRule.port(), mockSpan.tags().get(Tags.PEER_PORT.getKey()));
        Assert.assertEquals(0, mockSpan.logEntries().size());
    }

    @Test
    public void testInject() {
        final String path = "/foo";
        final String url = wireMockRule.url(path);
        stubFor(get(urlPathEqualTo(path))
                .willReturn(ok()
                        .withHeader("traceId", "{{request.headers.traceid}}")
                        .withHeader("spanId", "{{request.headers.spanid}}")));

        ResponseEntity<String> responseEntity = client.getForEntity(url, String.class);

        List<MockSpan> mockSpans = mockTracer.finishedSpans();
        Assert.assertEquals(1, mockSpans.size());
        Assert.assertEquals(mockSpans.get(0).context().traceId(),
                Long.parseLong(responseEntity.getHeaders().getFirst("traceId")));
        Assert.assertEquals(mockSpans.get(0).context().spanId(),
                Long.parseLong(responseEntity.getHeaders().getFirst("spanId")));
    }

    @Test
    public void testParentSpan() {
        {
            Span parent = mockTracer.buildSpan("parent").start();

            final String path = "/foo";
            final String url = wireMockRule.url(path);
            stubFor(get(urlPathEqualTo(path))
                    .willReturn(ok()));
            try (Scope scope = mockTracer.activateSpan(parent)) {
                client.getForEntity(url, String.class);
            } finally {
                parent.finish();
            }
        }

        List<MockSpan> mockSpans = mockTracer.finishedSpans();
        Assert.assertEquals(2, mockSpans.size());
        Assert.assertEquals(mockSpans.get(0).parentId(), mockSpans.get(1).context().spanId());
        Assert.assertEquals(mockSpans.get(0).context().traceId(), mockSpans.get(1).context().traceId());
    }

    @Test
    public void testErrorUnknownHostException() {
        String url = "http://nonexisting.example.com";
        try {
            client.getForEntity(url, String.class);
        } catch (RuntimeException ex) {
            Assert.assertTrue(NestedExceptionUtils.getRootCause(ex) instanceof UnknownHostException);
            //ok UnknownHostException
        }

        await().atMost(5, SECONDS).until(() -> {
            List<MockSpan> mockSpans = mockTracer.finishedSpans();
            return mockSpans.size() == 1;
        });

        List<MockSpan> mockSpans = mockTracer.finishedSpans();
        Assert.assertEquals(1, mockSpans.size());

        MockSpan mockSpan = mockSpans.get(0);
        Assert.assertEquals("GET", mockSpan.operationName());
        Assert.assertEquals(5, mockSpan.tags().size());
        Assert.assertEquals(componentName, mockSpan.tags().get(Tags.COMPONENT.getKey()));
        Assert.assertEquals(Tags.SPAN_KIND_CLIENT, mockSpan.tags().get(Tags.SPAN_KIND.getKey()));
        Assert.assertEquals("GET", mockSpan.tags().get(Tags.HTTP_METHOD.getKey()));
        Assert.assertEquals(url, mockSpan.tags().get(Tags.HTTP_URL.getKey()));
        Assert.assertEquals(Boolean.TRUE, mockSpan.tags().get(Tags.ERROR.getKey()));

        Assert.assertEquals(1, mockSpan.logEntries().size());
        Assert.assertEquals(2, mockSpan.logEntries().get(0).fields().size());
        Assert.assertEquals(Tags.ERROR.getKey(), mockSpan.logEntries().get(0).fields().get("event"));
        Assert.assertNotNull(mockSpan.logEntries().get(0).fields().get("error.object"));
    }
}
