/**
 *  Copyright 2015 Paul Querna
 *
 *  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 cacheobject

import (
	"github.com/stretchr/testify/require"

	"net/http"
	"testing"
	"time"
)

func TestCachableStatusCode(t *testing.T) {
	ok := []int{200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501}
	for _, v := range ok {
		require.True(t, cachableStatusCode(v), "status code should be cacheable: %d", v)
	}

	notok := []int{201, 429, 500, 504}
	for _, v := range notok {
		require.False(t, cachableStatusCode(v), "status code should not be cachable: %d", v)
	}
}

func fill(t *testing.T, now time.Time) Object {
	RespDirectives, err := ParseResponseCacheControl("")
	require.NoError(t, err)
	ReqDirectives, err := ParseRequestCacheControl("")
	require.NoError(t, err)

	obj := Object{
		RespDirectives: RespDirectives,
		RespHeaders:    http.Header{},
		RespStatusCode: 200,
		RespDateHeader: now,

		ReqDirectives: ReqDirectives,
		ReqHeaders:    http.Header{},
		ReqMethod:     "GET",

		NowUTC: now,
	}

	return obj
}

func TestGETPrivate(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	RespDirectives, err := ParseResponseCacheControl("private")
	require.NoError(t, err)

	obj.RespDirectives = RespDirectives

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonResponsePrivate)
}

func TestGETPrivateWithPrivateCache(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	RespDirectives, err := ParseResponseCacheControl("private")
	require.NoError(t, err)

	obj.CacheIsPrivate = true
	obj.RespDirectives = RespDirectives

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)
}

func TestUncachableMethods(t *testing.T) {
	type methodPair struct {
		m string
		r Reason
	}

	tc := []methodPair{
		{"PUT", ReasonRequestMethodPUT},
		{"DELETE", ReasonRequestMethodDELETE},
		{"CONNECT", ReasonRequestMethodCONNECT},
		{"OPTIONS", ReasonRequestMethodOPTIONS},
		{"CONNECT", ReasonRequestMethodCONNECT},
		{"TRACE", ReasonRequestMethodTRACE},
		{"MADEUP", ReasonRequestMethodUnknown},
	}

	for _, mp := range tc {
		now := time.Now().UTC()

		obj := fill(t, now)
		obj.ReqMethod = mp.m

		rv := ObjectResults{}
		CachableObject(&obj, &rv)
		require.NoError(t, rv.OutErr)
		require.Len(t, rv.OutReasons, 1)
		require.Contains(t, rv.OutReasons, mp.r)
	}
}

func TestHEAD(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "HEAD"
	obj.RespLastModifiedHeader = now.Add(time.Hour * -1)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)

	ExpirationObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)
	require.False(t, rv.OutExpirationTime.IsZero())
}

func TestHEADLongLastModified(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "HEAD"
	obj.RespLastModifiedHeader = now.Add(time.Hour * -70000)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)

	ExpirationObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)
	require.False(t, rv.OutExpirationTime.IsZero())
	require.WithinDuration(t, now.Add(twentyFourHours), rv.OutExpirationTime, time.Second*60)
}

func TestNonCachablePOST(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "POST"

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonRequestMethodPOST)
}

func TestCachablePOSTExpiresHeader(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "POST"
	obj.RespExpiresHeader = now.Add(time.Hour * 1)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)
}

func TestCachablePOSTSMax(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "POST"
	obj.RespDirectives.SMaxAge = DeltaSeconds(900)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)
}

func TestNonCachablePOSTSMax(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "POST"
	obj.CacheIsPrivate = true
	obj.RespDirectives.SMaxAge = DeltaSeconds(900)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonRequestMethodPOST)
}

func TestCachablePOSTMax(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "POST"
	obj.RespDirectives.MaxAge = DeltaSeconds(9000)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)
}

func TestPUTs(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "PUT"

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonRequestMethodPUT)
}

func TestPUTWithExpires(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqMethod = "PUT"
	obj.RespExpiresHeader = now.Add(time.Hour * 1)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonRequestMethodPUT)
}

func TestAuthorization(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqHeaders.Set("Authorization", "bearer random")

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonRequestAuthorizationHeader)
}

func TestCachableAuthorization(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqHeaders.Set("Authorization", "bearer random")
	obj.RespDirectives.Public = true
	obj.RespDirectives.MaxAge = DeltaSeconds(300)

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.NoError(t, rv.OutErr)
	require.Len(t, rv.OutReasons, 0)
}

func TestRespNoStore(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.RespDirectives.NoStore = true

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonResponseNoStore)
}

func TestReqNoStore(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.ReqDirectives.NoStore = true

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonRequestNoStore)
}

func TestResp500(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.RespStatusCode = 500

	rv := ObjectResults{}
	CachableObject(&obj, &rv)
	require.Len(t, rv.OutReasons, 1)
	require.Contains(t, rv.OutReasons, ReasonResponseUncachableByDefault)
}

func TestExpirationSMaxShared(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.RespDirectives.SMaxAge = DeltaSeconds(60)

	rv := ObjectResults{}
	ExpirationObject(&obj, &rv)
	require.Len(t, rv.OutWarnings, 0)
	require.WithinDuration(t, now.Add(time.Second*60), rv.OutExpirationTime, time.Second*1)
}

func TestExpirationSMaxPrivate(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.CacheIsPrivate = true
	obj.RespDirectives.SMaxAge = DeltaSeconds(60)

	rv := ObjectResults{}
	ExpirationObject(&obj, &rv)
	require.Len(t, rv.OutWarnings, 0)
	require.True(t, rv.OutExpirationTime.IsZero())
}

func TestExpirationMax(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	obj.RespDirectives.MaxAge = DeltaSeconds(60)

	rv := ObjectResults{}
	ExpirationObject(&obj, &rv)
	require.Len(t, rv.OutWarnings, 0)
	require.WithinDuration(t, now.Add(time.Second*60), rv.OutExpirationTime, time.Second*1)
}

func TestExpirationMaxAndSMax(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	// cache should select the SMax age since this is a shared cache.
	obj.RespDirectives.MaxAge = DeltaSeconds(60)
	obj.RespDirectives.SMaxAge = DeltaSeconds(900)

	rv := ObjectResults{}
	ExpirationObject(&obj, &rv)
	require.Len(t, rv.OutWarnings, 0)
	require.WithinDuration(t, now.Add(time.Second*900), rv.OutExpirationTime, time.Second*1)
}

func TestExpirationExpires(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	// cache should select the SMax age since this is a shared cache.
	obj.RespExpiresHeader = now.Add(time.Second * 1500)

	rv := ObjectResults{}
	ExpirationObject(&obj, &rv)
	require.Len(t, rv.OutWarnings, 0)
	require.WithinDuration(t, now.Add(time.Second*1500), rv.OutExpirationTime, time.Second*1)
}

func TestExpirationExpiresNoServerDate(t *testing.T) {
	now := time.Now().UTC()

	obj := fill(t, now)
	// cache should select the SMax age since this is a shared cache.
	obj.RespDateHeader = time.Time{}
	obj.RespExpiresHeader = now.Add(time.Second * 1500)

	rv := ObjectResults{}
	ExpirationObject(&obj, &rv)
	require.Len(t, rv.OutWarnings, 0)
	require.WithinDuration(t, now.Add(time.Second*1500), rv.OutExpirationTime, time.Second*1)
}

func TestCachableRequestObject(t *testing.T) {
	ReqDirectives, err := ParseRequestCacheControl("")
	require.NoError(t, err)

	obj := Object{
		ReqDirectives: ReqDirectives,
		ReqHeaders:    http.Header{},
		ReqMethod:     "GET",

		NowUTC: time.Now().UTC(),
	}

	rv := ObjectResults{}
	CachableRequestObject(&obj, &rv)
	require.Len(t, rv.OutReasons, 0)

	obj.ReqMethod = "PUT"
	rv.OutReasons = nil
	CachableRequestObject(&obj, &rv)
	require.Len(t, rv.OutReasons, 1)
}

func TestCachableResponseObject(t *testing.T) {
	obj := fill(t, time.Now().UTC())
	obj.ReqMethod = "DELETE"
	obj.RespDirectives.NoStore = true

	rv := ObjectResults{}
	CachableRequestObject(&obj, &rv)
	require.Len(t, rv.OutReasons, 1)
	CachableResponseObject(&obj, &rv)
	require.Len(t, rv.OutReasons, 2)
}
