// Copyright 2022 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice, this
//     list of conditions and the following disclaimer.
//
//  2. Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package cov_test

import (
	"reflect"
	"strings"
	"testing"

	"dawn.googlesource.com/dawn/tools/src/cov"
)

var (
	fileA = "coverage/file/a"
	fileB = "coverage/file/b"
	fileC = "coverage/file/c"
	fileD = "coverage/file/c"

	span0 = cov.Span{cov.Location{3, 2}, cov.Location{3, 9}}
	span1 = cov.Span{cov.Location{4, 1}, cov.Location{5, 1}}
	span2 = cov.Span{cov.Location{5, 5}, cov.Location{5, 7}}
	span3 = cov.Span{cov.Location{7, 2}, cov.Location{7, 7}}
)

//                a
//        ╭───────┴───────╮
//        b               c
//    ╭───┴───╮       ╭───┴───╮
//    d       e       f       g
//  ╭─┴─╮   ╭─┴─╮   ╭─┴─╮   ╭─┴─╮
//  h   i   j   k   l   m   n   o
//     ╭┴╮ ╭┴╮ ╭┴╮ ╭┴╮ ╭┴╮ ╭╯
//     p q r s t u v w x y z
//

func TestTree(t *testing.T) {
	tree := &cov.Tree{}

	t.Log("Add 'b' with the coverage [0,1]")
	tree.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))

	//           [0,1]
	//            (a)
	//       ╭─────╯
	//       b

	checkSpans(t, tree.Spans(), span0, span1)
	checkTests(t, tree, `{a:{b}}`)
	checkCoverage(t, tree, fileA, `a:{[0,1]}`)

	t.Log("Add 'i' with the coverage [0,1]")
	tree.Add(cov.Path{"a", "b", "d", "i"}, coverage(fileA, span0, span1))

	//           [0,1]
	//            (a)
	//       ╭─────╯
	//       b
	//    ╭──╯
	//    d
	//    ╰─╮
	//      i
	checkSpans(t, tree.Spans(), span0, span1)
	checkTests(t, tree, `{a:{b:{d:{i}}}}`)
	checkCoverage(t, tree, fileA, `a:{[0,1]}`)

	t.Log("Add 'e' with the coverage [0,1,2]")
	tree.Add(cov.Path{"a", "b", "e"}, coverage(fileA, span0, span1, span2))

	//           [0,1]
	//            (a)
	//       ┏━━━━━┛
	//      (b)
	//    ╭──┺━━┓
	//    d    (e)[2]
	//    ╰─╮
	//      i
	checkSpans(t, tree.Spans(), span0, span1, span2)
	checkTests(t, tree, `{a:{b:{d:{i} e}}}`)
	checkCoverage(t, tree, fileA, `a:{[0,1] b:{e:{[2]}}}`)

	t.Log("Add 'n' with the coverage [0,3]")
	tree.Add(cov.Path{"a", "c", "g", "n"}, coverage(fileA, span0, span3))

	//            [0]
	//            (a)
	//       ┏━━━━━┻━━━━━┓
	//   [1](b)         (c)[3]
	//    ╭──┺━━┓        ╰──╮
	//    d    (e)[2]       g
	//    ╰─╮             ╭─╯
	//      i             n
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i}e}c:{g:{n}}}}`)
	checkCoverage(t, tree, fileA, `a:{[0] b:{[1] e:{[2]}} c:{[3]}}`)

	t.Log("Add 'o' with the coverage [0, 3]")
	tree.Add(cov.Path{"a", "c", "g", "o"}, coverage(fileA, span0, span3))

	//              [0]
	//              (a)
	//       ┏━━━━━━━┻━━━━━━━┓
	//   [1](b)             (c)[3]
	//    ╭──┺━━┓            ╰──╮
	//    d    (e)[2]           g
	//    ╰─╮                 ╭─┴─╮
	//      i                 n   o
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i}e}c:{g:{n o}}}}`)
	checkCoverage(t, tree, fileA, `a:{[0] b:{[1] e:{[2]}} c:{[3]}}`)

	t.Log("Add 'f' with the coverage [1]")
	tree.Add(cov.Path{"a", "c", "f"}, coverage(fileA, span1))

	//               (a)
	//       ┏━━━━━━━━┻━━━━━━━━┓
	// [0,1](b)               (c)
	//    ╭──┺━━┓           ┏━━┻━━┓
	//    d    (e)[2]   [1](f)   (g)[0,3]
	//    ╰─╮                   ╭─┴─╮
	//      i                   n   o
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i} e} c:{f g:{n o}}}}`)
	checkCoverage(t, tree, fileA, `a:{b:{[0,1] e:{[2]}} c:{f:{[1]} g:{[0,3]}}}`)

	t.Log("Add 'j' with the coverage [3]")
	tree.Add(cov.Path{"a", "b", "e", "j"}, coverage(fileA, span3))

	//                   (a)
	//           ┏━━━━━━━━┻━━━━━━━━┓
	//          (b)               (c)
	//       ┏━━━┻━━━┓          ┏━━┻━━┓
	// [0,1](d)     (e)[3]  [1](f)   (g)[0,3]
	//       ╰─╮   ╭─╯              ╭─┴─╮
	//         i   j                n   o
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i} e:{j}} c:{f g:{n o}}}}`)
	checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1]} g:{[0,3]}}}`)

	t.Log("Add 'k' with the coverage [3]")
	tree.Add(cov.Path{"a", "b", "e", "k"}, coverage(fileA, span3))

	//                   (a)
	//           ┏━━━━━━━━┻━━━━━━━━┓
	//          (b)               (c)
	//       ┏━━━┻━━━┓          ┏━━┻━━┓
	// [0,1](d)     (e)[3]  [1](f)   (g)[0,3]
	//       ╰─╮   ╭─┴─╮            ╭─┴─╮
	//         i   j   k            n   o
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f g:{n o}}}}`)
	checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1]} g:{[0,3]}}}`)

	t.Log("Add 'v' with the coverage [1,2]")
	tree.Add(cov.Path{"a", "c", "f", "l", "v"}, coverage(fileA, span1, span2))

	//                   (a)
	//           ┏━━━━━━━━┻━━━━━━━━━━┓
	//          (b)                 (c)
	//       ┏━━━┻━━━┓            ┏━━┻━━┓
	// [0,1](d)     (e)[3]  [1,2](f)   (g)[0,3]
	//       ╰─╮   ╭─┴─╮        ╭─╯   ╭─┴─╮
	//         i   j   k        l     n   o
	//                         ╭╯
	//                         v
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v}} g:{n o}}}}`)
	checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1,2]} g:{[0,3]}}}`)

	t.Log("Add 'x' with the coverage [1,2]")
	tree.Add(cov.Path{"a", "c", "f", "l", "x"}, coverage(fileA, span1, span2))

	//                   (a)
	//           ┏━━━━━━━━┻━━━━━━━━━━┓
	//          (b)                 (c)
	//       ┏━━━┻━━━┓            ┏━━┻━━┓
	// [0,1](d)     (e)[3]  [1,2](f)   (g)[0,3]
	//       ╰─╮   ╭─┴─╮        ╭─╯   ╭─┴─╮
	//         i   j   k        l     n   o
	//                         ╭┴╮
	//                         v x
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v x}} g:{n o}}}}`)
	checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1,2]} g:{[0,3]}}}`)

	t.Log("Add 'z' with the coverage [2]")
	tree.Add(cov.Path{"a", "c", "g", "n", "z"}, coverage(fileA, span2))

	//                   (a)
	//           ┏━━━━━━━━┻━━━━━━━━━━━━┓
	//          (b)                   (c)
	//       ┏━━━┻━━━┓            ┏━━━━┻━━━━┓
	// [0,1](d)     (e)[3]  [1,2](f)       (g)
	//       ╰─╮   ╭─┴─╮        ╭─╯       ┏━┻━┓
	//         i   j   k        l    [2](n) (o)[0,3]
	//                         ╭┴╮      ╭╯
	//                         v x      z
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v x}} g:{n: {z} o}}}}`)
	checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1,2]} g:{n:{[2]} o:{[0,3]}}}}`)

	tree.Optimize()

	//                   (a)
	//           ┏━━━━━━━━┻━━━━━━━━━━━━┓
	//          (b)                   (c)
	//       ┏━━━┻━━━┓            ┏━━━━┻━━━━┓
	//   <0>(d)     (e)[3]    <2>(f)       (g)
	//       ╰─╮   ╭─┴─╮        ╭─╯       ┏━┻━┓
	//         i   j   k        l    [2](n) (o)<1>
	//                         ╭┴╮      ╭╯
	//                         v x      z
	checkSpans(t, tree.Spans(), span0, span1, span2, span3)
	checkGroups(t, tree.FileSpanGroups(fileA), map[cov.SpanGroupID]cov.SpanGroup{
		0: cov.SpanGroup{Spans: spans(0, 1)},
		1: cov.SpanGroup{Spans: spans(0, 3)},
		2: cov.SpanGroup{Spans: spans(1, 2)},
	})
	checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v x}} g:{n: {z} o}}}}`)
	checkCoverage(t, tree, fileA, `a:{b:{d:{<0>} e:{[3]}} c:{f:{<2>} g:{n:{[2]} o:{<1>}}}}`)
}

func TestTreeOptInvertForCommon(t *testing.T) {
	tree := &cov.Tree{}

	tree.Add(cov.Path{"a", "b"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "c"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "d"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "e"}, coverage(fileA, span1))
	tree.Add(cov.Path{"a", "f"}, coverage(fileA, span1))
	tree.Add(cov.Path{"a", "g"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "h"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "i"}, coverage(fileA, span0))

	//               (a)
	//  ┏━━━┳━━━┳━━━┳━┻━┳━━━┳━━━┳━━━┓
	// (b) (c) (d) (e) (f) (g) (h) (i)
	// [0] [0] [0] [1] [1] [0] [0] [0]
	checkSpans(t, tree.Spans(), span0, span1)
	checkTests(t, tree, `{a:{b c d e f g h i}}`)
	checkCoverage(t, tree, fileA, `a:{b:{[0]} c:{[0]} d:{[0]} e:{[1]} f:{[1]} g:{[0]} h:{[0]} i:{[0]}}`)

	tree.Optimize()

	//               [0]
	//               (a)
	//  ╭───┬───┬───┲━┻━┱───┬───┬───╮
	//  b   c   d  ┏┛   ┗┓  g   h   i
	//            (e)   (f)
	//            <0>   <0>
	checkSpans(t, tree.Spans(), span0, span1)
	checkGroups(t, tree.FileSpanGroups(fileA), map[cov.SpanGroupID]cov.SpanGroup{
		0: cov.SpanGroup{Spans: spans(0, 1)},
	})
	checkTests(t, tree, `{a:{b c d e f g h i}}`)
	checkCoverage(t, tree, fileA, `a:{[0] e:{<0>} f:{<0>}}`)
}

func TestTreeOptDontInvertForCommon(t *testing.T) {
	tree := &cov.Tree{}

	tree.Add(cov.Path{"a", "b"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "c"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "d"}, coverage(fileA, span0))
	tree.Add(cov.Path{"a", "e"}, coverage(fileA, span1))
	tree.Add(cov.Path{"a", "f"}, coverage(fileA, span1))
	tree.Add(cov.Path{"a", "g"}, coverage(fileA, span2))
	tree.Add(cov.Path{"a", "h"}, coverage(fileA, span2))
	tree.Add(cov.Path{"a", "i"}, coverage(fileA, span2))

	//               (a)
	//  ┏━━━┳━━━┳━━━┳━┻━┳━━━┳━━━┳━━━┓
	// (b) (c) (d) (e) (f) (g) (h) (i)
	// [0] [0] [0] [1] [1] [2] [2] [2]
	checkSpans(t, tree.Spans(), span0, span1, span2)
	checkTests(t, tree, `{a:{b c d e f g h i}}`)
	checkCoverage(t, tree, fileA, `a:{b:{[0]} c:{[0]} d:{[0]} e:{[1]} f:{[1]} g:{[2]} h:{[2]} i:{[2]}}`)

	tree.Optimize()

	//               (a)
	//  ┏━━━┳━━━┳━━━┳━┻━┳━━━┳━━━┳━━━┓
	// (b) (c) (d) (e) (f) (g) (h) (i)
	// [0] [0] [0] [1] [1] [2] [2] [2]
	checkSpans(t, tree.Spans(), span0, span1, span2)
	checkTests(t, tree, `{a:{b c d e f g h i}}`)
	checkCoverage(t, tree, fileA, `a:{b:{[0]} c:{[0]} d:{[0]} e:{[1]} f:{[1]} g:{[2]} h:{[2]} i:{[2]}}`)
}

func checkSpans(t *testing.T, got []cov.Span, expect ...cov.Span) {
	if !reflect.DeepEqual(got, expect) {
		t.Errorf("Spans not as expected.\nGot:    %+v\nExpect: %+v", got, expect)
	}
}

func checkGroups(t *testing.T, got, expect map[cov.SpanGroupID]cov.SpanGroup) {
	if !reflect.DeepEqual(got, expect) {
		t.Errorf("SpanGroupss not as expected.\nGot:    %+v\nExpect: %+v", got, expect)
	}
}

func checkTests(t *testing.T, tree *cov.Tree, expect string) {
	g, e := tree.Tests().String(tree.Strings()), expect
	if tg, te := trimWS(g), trimWS(e); tg != te {
		t.Errorf("Tests not as expected.\nGot:\n%v\nExpect:\n%v\n------\nGot:    %v\nExpect: %v", g, e, tg, te)
	}
}

func checkCoverage(t *testing.T, tree *cov.Tree, file string, expect string) {
	g, e := tree.FileCoverage(file).String(tree.Tests(), tree.Strings()), expect
	if tg, te := trimWS(g), trimWS(e); tg != te {
		t.Errorf("Coverage not as expected.\nGot:\n%v\nExpect:\n%v\n------\nGot:    %v\nExpect: %v", g, e, tg, te)
	}
}

func trimWS(s string) string {
	s = strings.ReplaceAll(s, " ", "")
	s = strings.ReplaceAll(s, "\n", "")
	return s
}

func coverage(file string, spans ...cov.Span) *cov.Coverage {
	return &cov.Coverage{
		[]cov.File{
			cov.File{
				Path:    file,
				Covered: spans,
			},
		},
	}
}

func spans(ids ...cov.SpanID) cov.SpanSet {
	out := make(cov.SpanSet, len(ids))
	for _, id := range ids {
		out[id] = struct{}{}
	}
	return out
}

func TestTreeEncodeDecode(t *testing.T) {
	orig := &cov.Tree{}
	orig.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))
	orig.Add(cov.Path{"a", "b", "d", "i"}, coverage(fileA, span0, span1))
	orig.Add(cov.Path{"a", "b", "e"}, coverage(fileA, span0, span1, span2))
	orig.Add(cov.Path{"a", "c", "g", "n"}, coverage(fileB, span0, span3))
	orig.Add(cov.Path{"a", "c", "g", "o"}, coverage(fileB, span0, span3))
	orig.Add(cov.Path{"a", "c", "f"}, coverage(fileA, span1))
	orig.Add(cov.Path{"a", "b", "e", "j"}, coverage(fileC, span3))
	orig.Add(cov.Path{"a", "b", "e", "k"}, coverage(fileA, span3))
	orig.Add(cov.Path{"a", "c", "f", "l", "v"}, coverage(fileA, span1, span2))
	orig.Add(cov.Path{"a", "c", "f", "l", "x"}, coverage(fileA, span1, span2))
	orig.Add(cov.Path{"a", "c", "g", "n", "z"}, coverage(fileC, span2))
	orig.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))
	orig.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))

	origJSON := orig.JSON("revision goes here")
	read, revision, err := cov.ReadJSON(strings.NewReader(origJSON))
	if err != nil {
		t.Fatalf("cov.ReadJSON() failed with: %v", err)
	}
	readJSON := read.JSON(revision)
	if origJSON != readJSON {
		t.Fatalf("Encode -> Decode -> Encode produced different results:\nOriginal:\n\n%v\n\nRead:\n\n%v", origJSON, readJSON)
	}
}
