/*
 * Copyright 2018 The NATS 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 jwt

import (
	"sort"
	"testing"
	"time"
)

func TestSimpleExportValidation(t *testing.T) {
	e := &Export{Subject: "foo", Type: Stream}

	vr := CreateValidationResults()
	e.Validate(vr)

	if !vr.IsEmpty() {
		t.Errorf("simple export should validate cleanly")
	}

	e.Type = Service
	vr = CreateValidationResults()
	e.Validate(vr)

	if !vr.IsEmpty() {
		t.Errorf("simple export should validate cleanly")
	}
}

func TestResponseTypeValidation(t *testing.T) {
	e := &Export{Subject: "foo", Type: Stream, ResponseType: ResponseTypeSingleton}

	vr := CreateValidationResults()
	e.Validate(vr)

	if vr.IsEmpty() {
		t.Errorf("response type on stream should have an validation issue")
	}
	if e.IsSingleResponse() {
		t.Errorf("response type should always fail for stream")
	}

	e.Type = Service
	vr = CreateValidationResults()
	e.Validate(vr)
	if !vr.IsEmpty() {
		t.Errorf("response type on service should validate cleanly")
	}
	if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() {
		t.Errorf("response type should be single")
	}

	e.ResponseType = ResponseTypeChunked
	vr = CreateValidationResults()
	e.Validate(vr)
	if !vr.IsEmpty() {
		t.Errorf("response type on service should validate cleanly")
	}
	if e.IsSingleResponse() || !e.IsChunkedResponse() || e.IsStreamResponse() {
		t.Errorf("response type should be chunk")
	}

	e.ResponseType = ResponseTypeStream
	vr = CreateValidationResults()
	e.Validate(vr)
	if !vr.IsEmpty() {
		t.Errorf("response type on service should validate cleanly")
	}
	if e.IsSingleResponse() || e.IsChunkedResponse() || !e.IsStreamResponse() {
		t.Errorf("response type should be stream")
	}

	e.ResponseType = ""
	vr = CreateValidationResults()
	e.Validate(vr)
	if !vr.IsEmpty() {
		t.Errorf("response type on service should validate cleanly")
	}
	if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() {
		t.Errorf("response type should be single")
	}

	e.ResponseType = "bad"
	vr = CreateValidationResults()
	e.Validate(vr)
	if vr.IsEmpty() {
		t.Errorf("response type should match available options")
	}
	if e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() {
		t.Errorf("response type should be bad")
	}
}

func TestInvalidExportType(t *testing.T) {
	i := &Export{Subject: "foo", Type: Unknown}

	vr := CreateValidationResults()
	i.Validate(vr)

	if vr.IsEmpty() {
		t.Errorf("export with bad type should not validate cleanly")
	}

	if !vr.IsBlocking(true) {
		t.Errorf("invalid type is blocking")
	}
}

func TestOverlappingExports(t *testing.T) {
	i := &Export{Subject: "bar.foo", Type: Stream}
	i2 := &Export{Subject: "bar.*", Type: Stream}

	exports := &Exports{}
	exports.Add(i, i2)

	vr := CreateValidationResults()
	exports.Validate(vr)

	if len(vr.Issues) != 1 {
		t.Errorf("export has overlapping subjects")
	}
}

func TestDifferentExportTypes_OverlapOK(t *testing.T) {
	i := &Export{Subject: "bar.foo", Type: Service}
	i2 := &Export{Subject: "bar.*", Type: Stream}

	exports := &Exports{}
	exports.Add(i, i2)

	vr := CreateValidationResults()
	exports.Validate(vr)

	if len(vr.Issues) != 0 {
		t.Errorf("should allow overlaps on different export kind")
	}
}

func TestDifferentExportTypes_SameSubjectOK(t *testing.T) {
	i := &Export{Subject: "bar", Type: Service}
	i2 := &Export{Subject: "bar", Type: Stream}

	exports := &Exports{}
	exports.Add(i, i2)

	vr := CreateValidationResults()
	exports.Validate(vr)

	if len(vr.Issues) != 0 {
		t.Errorf("should allow overlaps on different export kind")
	}
}

func TestSameExportType_SameSubject(t *testing.T) {
	i := &Export{Subject: "bar", Type: Service}
	i2 := &Export{Subject: "bar", Type: Service}

	exports := &Exports{}
	exports.Add(i, i2)

	vr := CreateValidationResults()
	exports.Validate(vr)

	if len(vr.Issues) != 1 {
		t.Errorf("should not allow same subject on same export kind")
	}
}

func TestExportRevocation(t *testing.T) {
	akp := createAccountNKey(t)
	apk := publicKey(akp, t)
	account := NewAccountClaims(apk)
	e := &Export{Subject: "foo", Type: Stream}

	account.Exports.Add(e)

	pubKey := "bar"
	now := time.Now()

	// test that clear is safe before we add any
	e.ClearRevocation(pubKey)

	if e.IsRevokedAt(pubKey, now) {
		t.Errorf("no revocation was added so is revoked should be false")
	}

	e.RevokeAt(pubKey, now.Add(time.Second*100))

	if !e.IsRevokedAt(pubKey, now) {
		t.Errorf("revocation should hold when timestamp is in the future")
	}

	if e.IsRevokedAt(pubKey, now.Add(time.Second*150)) {
		t.Errorf("revocation should time out")
	}

	e.RevokeAt(pubKey, now.Add(time.Second*50)) // shouldn't change the revocation, you can't move it in

	if !e.IsRevokedAt(pubKey, now.Add(time.Second*60)) {
		t.Errorf("revocation should hold, 100 > 50")
	}

	encoded, _ := account.Encode(akp)
	decoded, _ := DecodeAccountClaims(encoded)

	if !decoded.Exports[0].IsRevokedAt(pubKey, now.Add(time.Second*60)) {
		t.Errorf("revocation should last across encoding")
	}

	e.ClearRevocation(pubKey)

	if e.IsRevokedAt(pubKey, now) {
		t.Errorf("revocations should be cleared")
	}

	e.RevokeAt(pubKey, now.Add(time.Second*1000))

	if !e.IsRevoked(pubKey) {
		t.Errorf("revocation be true we revoked in the future")
	}
}

func TestExportTrackLatency(t *testing.T) {
	e := &Export{Subject: "foo", Type: Service}
	e.Latency = &ServiceLatency{Sampling: 100, Results: "results"}
	vr := CreateValidationResults()
	e.Validate(vr)
	if !vr.IsEmpty() {
		t.Errorf("Expected to validate with simple tracking")
	}

	e = &Export{Subject: "foo", Type: Stream}
	e.Latency = &ServiceLatency{Sampling: 100, Results: "results"}
	vr = CreateValidationResults()
	e.Validate(vr)
	if vr.IsEmpty() {
		t.Errorf("adding latency tracking to a stream should have an validation issue")
	}

	e = &Export{Subject: "foo", Type: Service}
	e.Latency = &ServiceLatency{Sampling: 0, Results: "results"}
	vr = CreateValidationResults()
	e.Validate(vr)
	if vr.IsEmpty() {
		t.Errorf("Sampling <1 should have a validation issue")
	}

	e = &Export{Subject: "foo", Type: Service}
	e.Latency = &ServiceLatency{Sampling: 122, Results: "results"}
	vr = CreateValidationResults()
	e.Validate(vr)
	if vr.IsEmpty() {
		t.Errorf("Sampling >100 should have a validation issue")
	}

	e = &Export{Subject: "foo", Type: Service}
	e.Latency = &ServiceLatency{Sampling: 22, Results: "results.*"}
	vr = CreateValidationResults()
	e.Validate(vr)
	if vr.IsEmpty() {
		t.Errorf("Results subject needs to be valid publish subject")
	}
}

func TestExport_Sorting(t *testing.T) {
	var exports Exports
	exports.Add(&Export{Subject: "x", Type: Service})
	exports.Add(&Export{Subject: "z", Type: Service})
	exports.Add(&Export{Subject: "y", Type: Service})
	if exports[0].Subject != "x" {
		t.Fatal("added export not in expected order")
	}
	sort.Sort(exports)
	if exports[0].Subject != "x" && exports[1].Subject != "y" && exports[2].Subject != "z" {
		t.Fatal("exports not sorted")
	}
}
