/*
Copyright 2019 The Knative 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 sampleconsistency

import (
	"bufio"
	"fmt"
	"os"
	"path"
	"strings"
	"testing"

	"github.com/knative/docs/test"
	"github.com/knative/docs/test/sampleapp"
)

const (
	configFile = "../sampleapp/config.yaml"
)

func readlines(t *testing.T, filename string) []string {
	var res []string
	f, err := os.Open(filename)
	if nil != err {
		t.Fatalf("Failed opening file '%s': '%v'", filename, err)
	}
	defer f.Close()
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		res = append(res, scanner.Text())
	}
	if err = scanner.Err(); nil != err {
		t.Fatalf("Failed reading file '%s': '%v'", filename, err)
	}
	return res
}

func normalize(si string) string {
	return strings.Trim(si, " `\t\r\n")
}

func checkContains(t *testing.T, rl []string, src string) {
	sl := readlines(t, src)
	ir := 0
	is := 0
	best := -1
	// Scans rl(README lines) for entire block matching sl(source lines).
	// Pointer ir: keeps on moving no matter what.
	// Pointer is: moves only when there is a line match, otherwise back to 0. A
	//   match is found if pointer is moved to end.
	// best: tracks where the best match is on source lines, it's always one
	//   more line ahead of real match
	for ir < len(rl) && is < len(sl) {
		nr := normalize(rl[ir])
		ns := normalize(sl[is])

		if "" == ns {
			is++
			if is > best { // Consider it a match if it's empty line
				best = is
			}
			continue
		}
		if "" == nr {
			ir++
			continue
		}
		if nr != ns { // Start over if a non-match is found
			is = 0
		} else {
			is++
			if is > best {
				best = is
			}
		}
		ir++
	}

	if best == -1 {
		// missing line is line 0
		best = 0
	}
	if best != len(sl) {
		t.Fatalf("README.md/index.md file is missing line %d ('%s') from file '%s'\nAdditional info:\n%s", best, sl[best], src, sampleapp.ActionMsg)
	}
}

func checkDoc(t *testing.T, lc sampleapp.LanguageConfig) {
	readme, err := getDocFile(lc.SrcDir)
	if err != nil {
		t.Fatalf("Error: %v.", err)
		return
	}
	rl := readlines(t, readme)
	for _, f := range lc.Copies {
		src := path.Join(lc.SrcDir, f)
		checkContains(t, rl, src)
	}
}

func getDocFile(dir string) (string, error) {
	for _, f := range []string{"README.md", "index.md"} {
		path := path.Join(dir, f)
		_, err := os.Stat(path)
		if err == nil {
			return path, nil
		}
		if !os.IsNotExist(err) {
			return "", nil
		}
	}
	return "", fmt.Errorf("cannot find README.md or index.md in %s", dir)
}

// TestDocSrc checks content of README.md files, and ensures that the real code of the samples
// is properly embedded in the docs.
func TestDocSrc(t *testing.T) {
	lcs, err := sampleapp.GetConfigs(configFile)
	if nil != err {
		t.Fatalf("Failed reading config file %s: '%v'", configFile, err)
	}

	allowed := test.GetAllowedLanguages()
	for _, lc := range lcs.Languages {
		if _, ok := allowed[lc.Language]; len(allowed) > 0 && !ok {
			continue
		}
		lc.UseDefaultIfNotProvided()
		t.Run(lc.Language, func(t *testing.T) {
			checkDoc(t, lc)
		})
	}
}
