package arxiv

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

const paperFeed = `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"
      xmlns:arxiv="http://arxiv.org/schemas/atom">
  <opensearch:totalResults>12</opensearch:totalResults>
  <opensearch:startIndex>5</opensearch:startIndex>
  <opensearch:itemsPerPage>2</opensearch:itemsPerPage>
  <entry>
    <id>https://arxiv.org/abs/1605.08386v1</id>
    <updated>2016-05-26T17:59:46Z</updated>
    <published>2016-05-26T17:59:46Z</published>
    <title>
      Heat bath   random walks
    </title>
    <summary>  Abstract
body.  </summary>
    <author><name>Alice</name><arxiv:affiliation>Lab One</arxiv:affiliation></author>
    <author><name>Bob</name></author>
    <arxiv:comment>10 pages</arxiv:comment>
    <arxiv:journal_ref>Journal 1</arxiv:journal_ref>
    <arxiv:doi>10.1000/example</arxiv:doi>
    <arxiv:primary_category term="math.PR"/>
    <category term="math.PR"/>
    <category term="cs.DS"/>
    <link href="https://arxiv.org/abs/1605.08386v1" rel="alternate" type="text/html"/>
    <link href="https://arxiv.org/pdf/1605.08386v1" title="pdf" rel="related" type="application/pdf"/>
  </entry>
  <entry>
    <id>https://arxiv.org/abs/1605.00000v1</id>
    <updated>not-a-time</updated>
    <published>2016-05-26T17:59:46Z</published>
  </entry>
</feed>`

type roundTripFunc func(*http.Request) (*http.Response, error)

func (fn roundTripFunc) Do(request *http.Request) (*http.Response, error) {
	return fn(request)
}

func response(
	request *http.Request,
	status int,
	contentType string,
	body []byte,
) *http.Response {
	header := make(http.Header)
	if contentType != "" {
		header.Set("Content-Type", contentType)
	}
	return &http.Response{
		StatusCode: status,
		Header:     header,
		Body:       io.NopCloser(bytes.NewReader(body)),
		Request:    request,
	}
}

func TestDefaultHTTPClientDisablesAmbientProxyAndRedirects(t *testing.T) {
	client := defaultHTTPClient()
	transport, ok := client.Transport.(*http.Transport)
	require.True(t, ok)
	require.Nil(t, transport.Proxy)
	require.NotNil(t, client.CheckRedirect)
	require.ErrorIs(
		t,
		client.CheckRedirect(
			&http.Request{URL: &url.URL{
				Scheme: "https",
				Host:   "export.arxiv.org",
			}},
			nil,
		),
		http.ErrUseLastResponse,
	)
}

func TestSearchMatchesPythonQueryAndAtomSemantics(t *testing.T) {
	var requested *url.URL
	service := &service{
		endpoint: apiEndpoint,
		client: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			requested = request.URL
			return response(
				request,
				http.StatusOK,
				"application/atom+xml",
				[]byte(paperFeed),
			), nil
		}),
	}
	result, err := service.Search(context.Background(), SearchRequest{
		Query:     `au:del_maestro AND ti:"checkerboard walks"`,
		IDList:    []string{"1605.08386v1"},
		Offset:    5,
		Limit:     2,
		SortBy:    SortBySubmittedDate,
		SortOrder: SortOrderAscending,
	})
	require.NoError(t, err)
	require.Equal(t, `au:del_maestro AND ti:"checkerboard walks"`, requested.Query().Get("search_query"))
	require.Equal(t, "1605.08386v1", requested.Query().Get("id_list"))
	require.Equal(t, "5", requested.Query().Get("start"))
	require.Equal(t, "2", requested.Query().Get("max_results"))
	require.Equal(t, "submittedDate", requested.Query().Get("sortBy"))
	require.Equal(t, "ascending", requested.Query().Get("sortOrder"))

	require.Equal(t, 12, result.TotalResults)
	require.Equal(t, 5, result.Offset)
	require.Equal(t, 1, result.Returned, "malformed item is skipped independently")
	require.True(t, result.HasMore)
	require.NotNil(t, result.NextOffset)
	require.Equal(t, 7, *result.NextOffset, "next offset counts consumed Atom entries")
	require.Len(t, result.Papers, 1)
	paper := result.Papers[0]
	require.Equal(t, "1605.08386v1", paper.ID)
	require.Equal(t, " Heat bath random walks ", paper.Title)
	require.Equal(t, "  Abstract\nbody.  ", paper.Abstract)
	require.Equal(t, []Author{
		{Name: "Alice", Affiliations: []string{"Lab One"}},
		{Name: "Bob", Affiliations: []string{}},
	}, paper.Authors)
	require.Equal(t, "math.PR", paper.PrimaryCategory)
	require.Equal(t, []string{"math.PR", "cs.DS"}, paper.Categories)
	require.Equal(t, "https://arxiv.org/pdf/1605.08386v1", paper.PDFURL)
	require.Equal(t, "https://arxiv.org/src/1605.08386v1", paper.SourceURL)
}

func TestSearchSupportsIDListWithoutQuery(t *testing.T) {
	service := &service{
		endpoint: apiEndpoint,
		client: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			require.Empty(t, request.URL.Query().Get("search_query"))
			require.Equal(
				t,
				"1605.08386v1,quant-ph/0201082v1",
				request.URL.Query().Get("id_list"),
			)
			feed := strings.ReplaceAll(
				strings.ReplaceAll(
					paperFeed,
					"<opensearch:startIndex>5</opensearch:startIndex>",
					"<opensearch:startIndex>0</opensearch:startIndex>",
				),
				"<opensearch:totalResults>12</opensearch:totalResults>",
				"<opensearch:totalResults>2</opensearch:totalResults>",
			)
			return response(
				request,
				http.StatusOK,
				"application/atom+xml",
				[]byte(feed),
			), nil
		}),
	}
	result, err := service.Search(context.Background(), SearchRequest{
		IDList: []string{"1605.08386v1", "quant-ph/0201082v1"},
		Limit:  2,
	})
	require.NoError(t, err)
	require.Equal(t, 2, result.TotalResults)
}

func TestSearchRejectsMissingQueryAndIDList(t *testing.T) {
	service := &service{}
	_, err := service.Search(context.Background(), SearchRequest{})
	require.Equal(t, ErrorValidation, CodeOf(err))
}

func TestSearchStopsPaginationAtOfficialResultWindow(t *testing.T) {
	service := &service{
		endpoint: apiEndpoint,
		client: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			feed := strings.ReplaceAll(
				strings.ReplaceAll(
					paperFeed,
					"<opensearch:startIndex>5</opensearch:startIndex>",
					"<opensearch:startIndex>29998</opensearch:startIndex>",
				),
				"<opensearch:totalResults>12</opensearch:totalResults>",
				"<opensearch:totalResults>35000</opensearch:totalResults>",
			)
			return response(
				request,
				http.StatusOK,
				"application/atom+xml",
				[]byte(feed),
			), nil
		}),
	}
	result, err := service.Search(context.Background(), SearchRequest{
		Query:  "all:agentteam",
		Offset: 29_998,
		Limit:  2,
	})
	require.NoError(t, err)
	require.False(t, result.HasMore)
	require.Nil(t, result.NextOffset)
}

func TestSearchValidationAndUpstreamClassification(t *testing.T) {
	service := &service{
		endpoint: apiEndpoint,
		client: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			return response(
				request,
				http.StatusTooManyRequests,
				"text/plain",
				nil,
			), nil
		}),
	}
	_, err := service.Search(context.Background(), SearchRequest{
		Query:  "all:agentteam",
		Offset: 29_990,
		Limit:  20,
	})
	require.Equal(t, ErrorValidation, CodeOf(err))

	_, err = service.Search(context.Background(), SearchRequest{
		Query: "all:agentteam",
		Limit: 1,
	})
	require.Equal(t, ErrorRateLimited, CodeOf(err))
}

func TestAtomArtifactURLsRemainTrustedArxivHTTPS(t *testing.T) {
	feed := strings.ReplaceAll(
		paperFeed,
		"https://arxiv.org/pdf/1605.08386v1",
		"http://arxiv.org/pdf/1605.08386v1",
	)
	result, err := parseFeed([]byte(feed))
	require.NoError(t, err)
	require.Equal(
		t,
		"https://arxiv.org/pdf/1605.08386v1",
		result.Papers[0].PDFURL,
	)
	require.Equal(
		t,
		"https://arxiv.org/src/1605.08386v1",
		result.Papers[0].SourceURL,
	)

	untrusted := strings.ReplaceAll(
		paperFeed,
		"https://arxiv.org/pdf/1605.08386v1",
		"https://example.com/pdf/1605.08386v1",
	)
	result, err = parseFeed([]byte(untrusted))
	require.NoError(t, err)
	require.Empty(t, result.Papers[0].PDFURL)
	require.Empty(t, result.Papers[0].SourceURL)
}

func TestGetSupportsLatestVersionAndLegacyIDs(t *testing.T) {
	feed := strings.ReplaceAll(paperFeed, "<opensearch:startIndex>5</opensearch:startIndex>", "<opensearch:startIndex>0</opensearch:startIndex>")
	for _, id := range []string{"1605.08386", "1605.08386v1"} {
		t.Run(id, func(t *testing.T) {
			service := &service{
				endpoint: apiEndpoint,
				client: roundTripFunc(func(request *http.Request) (*http.Response, error) {
					require.Equal(t, id, request.URL.Query().Get("id_list"))
					return response(request, http.StatusOK, "application/atom+xml", []byte(feed)), nil
				}),
			}
			result, err := service.Get(context.Background(), GetRequest{ID: id})
			require.NoError(t, err)
			require.Equal(t, "1605.08386v1", result.Paper.ID)
		})
	}

	emptyFeed := strings.ReplaceAll(
		strings.ReplaceAll(feed, "<opensearch:totalResults>12</opensearch:totalResults>", "<opensearch:totalResults>0</opensearch:totalResults>"),
		feed[strings.Index(feed, "  <entry>"):strings.LastIndex(feed, "</feed>")],
		"",
	)
	service := &service{
		endpoint: apiEndpoint,
		client: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			return response(request, http.StatusOK, "application/atom+xml", []byte(emptyFeed)), nil
		}),
	}
	_, err := service.Get(context.Background(), GetRequest{ID: "quant-ph/0201082v1"})
	require.Equal(t, ErrorNotFound, CodeOf(err))
}
