// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package scandeps

import (
	"testing"

	"github.com/google/go-cmp/cmp"
)

func TestCPPScan(t *testing.T) {
	ctx := t.Context()
	for _, tc := range []struct {
		name         string
		buf          string
		wantIncludes []string
		wantDefines  map[string][]string
	}{
		{
			name: "helloworld",
			buf: `
#include <stdio.h>

int main(int arg, char *argv[]) {
  printf("hello, world\n");
}
`,
			wantIncludes: []string{
				"<stdio.h>",
			},
			wantDefines: map[string][]string{},
		},
		{
			name: "base",
			buf: `
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef BASE_VERSION_H_
#define BASE_VERSION_H_

#include <stdint.h>

#include <iosfwd>
#include <string>
#include <vector>

#include "base/base_export.h"
#include "base/strings/string_piece.h"

namespace base {
 ...
}
`,
			wantIncludes: []string{
				"<stdint.h>",
				"<iosfwd>",
				"<string>",
				"<vector>",
				`"base/base_export.h"`,
				`"base/strings/string_piece.h"`,
			},
			wantDefines: map[string][]string{},
		},
		{
			name: "macros",
			buf: `
#ifndef CONFIG_H
#define CONFIG_H

#ifdef NDEBUG
 # define USER_CONFIG_H "user_release.h"
#else
 # define USER_CONFIG_H "user_debug.h"
#endif

#include USER_CONFIG_H

#endif // CONFIG_H
`,
			wantIncludes: []string{
				"USER_CONFIG_H",
			},
			wantDefines: map[string][]string{
				"USER_CONFIG_H": {`"user_release.h"`, `"user_debug.h"`},
			},
		},
		{
			name: "unsupported",
			buf: `
#include /* comment */ "foo.h"
#include /* comment */ <bar.h>
#include \
 "baz.h"
# \
 include "quux.h"

#define FOO_H /* comment */ BAR_H
#define BAR_H \
 BAZ("baz")
#define BAZ(x) x # ".h"
`,
			wantDefines: map[string][]string{},
		},
		{
			name: "objc-import",
			buf: `
#import "HTTPRequest.h"
`,
			wantIncludes: []string{
				`"HTTPRequest.h"`,
			},
			wantDefines: map[string][]string{},
		},
		{
			name: "include-next",
			buf: `
#include_next <limits.h>
`,
			wantIncludes: []string{
				`<limits.h>`,
			},
			wantDefines: map[string][]string{},
		},
		{
			name: "no-include",
			buf: `
#include
`,
			wantDefines: map[string][]string{},
		},
		{
			name: "no-define",
			buf: `
#define
`,
			wantDefines: map[string][]string{},
		},
		{
			name: "no-directive",
			buf: `
#
`,
			wantDefines: map[string][]string{},
		},
		{
			name: "empty-line",
			buf: `
`,
			wantDefines: map[string][]string{},
		},
		{
			// b/465210078
			name: "no-space-qinclude",
			buf: `
#include"gf_error.h"
`,
			wantIncludes: []string{
				`"gf_error.h"`,
			},
			wantDefines: map[string][]string{},
		},
		{
			name: "no-space-include",
			buf: `
#include<stdio.h>
`,
			wantIncludes: []string{
				`<stdio.h>`,
			},
			wantDefines: map[string][]string{},
		},
		{
			name: "wrong-directive",
			buf: `
#define FOO "foo.h"
#includeFOO
`,
			wantDefines: map[string][]string{
				"FOO": {`"foo.h"`},
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			gotIncludes, gotDefines, err := CPPScan(ctx, tc.name, []byte(tc.buf))
			if err != nil {
				t.Errorf("CPPScan(ctx,%q,buf)=%v,%v,%v; want nil error", tc.name, gotIncludes, gotDefines, err)
			}
			if diff := cmp.Diff(tc.wantIncludes, gotIncludes); diff != "" {
				t.Errorf("CPPScan(ctx,%q,buf) includes diff -want +got:\n%s", tc.name, diff)
			}
			if diff := cmp.Diff(tc.wantDefines, gotDefines); diff != "" {
				t.Errorf("CPPScan(ctx,%q,buf) defines diff -want +got:\n%s", tc.name, diff)
			}
		})
	}
}

func TestAddInclude(t *testing.T) {
	ctx := t.Context()
	for _, tc := range []struct {
		name  string
		input string
		want  []string
	}{
		{
			name:  "bracket",
			input: "<foo.h>",
			want:  []string{"<foo.h>"},
		},
		{
			name:  "bracket-extra",
			input: "<foo.h> // comment",
			want:  []string{"<foo.h>"},
		},
		{
			name:  "quote",
			input: `"foo.h"`,
			want:  []string{`"foo.h"`},
		},
		{
			name:  "quote-extra",
			input: `"foo.h"  // comment`,
			want:  []string{`"foo.h"`},
		},
		{
			name:  "macro",
			input: "FOO_H",
			want:  []string{"FOO_H"},
		},
		{
			name:  "macro-extra",
			input: "FOO_H // comment",
			want:  []string{"FOO_H"},
		},
		{
			name:  "number",
			input: "1",
		},
		{
			name:  "lower-macro",
			input: "foo_h",
		},
		{
			name:  "comment",
			input: `/* comment */ "foo.h"`,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got := addInclude(ctx, nil, []byte(tc.input))
			if diff := cmp.Diff(tc.want, got); diff != "" {
				t.Errorf("addInclude(ctx, nil, %q): diff -want +got:\n%s", tc.input, diff)
			}
		})
	}
}

func TestAddDefine(t *testing.T) {
	ctx := t.Context()
	for _, tc := range []struct {
		name  string
		input string
		want  map[string][]string
	}{
		{
			name:  "string-literal",
			input: `FOO "foo.h"`,
			want: map[string][]string{
				"FOO": {`"foo.h"`},
			},
		},
		{
			name:  "bracket",
			input: "FOO <foo.h>",
			want: map[string][]string{
				"FOO": {"<foo.h>"},
			},
		},
		{
			name:  "OTHER_MACRO",
			input: "FOO BAR_H",
			want: map[string][]string{
				"FOO": {"BAR_H"},
			},
		},
		{
			name:  "other-value",
			input: "FOO foo",
			want:  map[string][]string{},
		},
		{
			name:  "number",
			input: "FOO 1",
			want:  map[string][]string{},
		},
		{
			name:  "lower-macro",
			input: "FOO macro_value",
			want:  map[string][]string{},
		},
		{
			name:  "func-macro",
			input: "FOO(x) BAR(x)",
			want:  map[string][]string{},
		},
		{
			name:  "comment",
			input: "FOO /* comment */ BAR",
			want:  map[string][]string{},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			defines := make(map[string][]string)
			addDefine(ctx, defines, []byte(tc.input))
			if diff := cmp.Diff(tc.want, defines); diff != "" {
				t.Errorf("addDefines(ctx,defines,file,%q): diff -want +got:\n%s", tc.input, diff)
			}
		})
	}
}

func TestExpandMacros(t *testing.T) {
	ctx := t.Context()

	for _, tc := range []struct {
		name    string
		incname string
		macros  map[string][]string
		want    []string
		wantErr bool
	}{
		{
			name: "empty",
		},
		{
			name:    "string-literal",
			incname: `"foo.h"`,
			want:    []string{`"foo.h"`},
		},
		{
			name:    "bracket",
			incname: "<foo.h>",
			want:    []string{"<foo.h>"},
		},
		{
			name:    "expand",
			incname: "FOO_H",
			macros: map[string][]string{
				"FOO_H": {`"foo.h"`},
			},
			want: []string{`"foo.h"`},
		},
		{
			name:    "expand-multi",
			incname: "FOO_H",
			macros: map[string][]string{
				"FOO_H": {`"foo.h"`, `"foo_debug.h"`},
			},
			want: []string{`"foo.h"`, `"foo_debug.h"`},
		},
		{
			name:    "expand-rec",
			incname: "FOO_H",
			macros: map[string][]string{
				"FOO_H": {"BAR_H"},
				"BAR_H": {`"bar.h"`},
			},
			want: []string{`"bar.h"`},
		},
		{
			name:    "func-macros",
			incname: "PATH(foo.h)",
			macros: map[string][]string{
				"PATH(x)": {"<dir/ ## x>"},
			},
			wantErr: true,
		},
		{
			name:    "expand-func-macro",
			incname: "INCNAME",
			macros: map[string][]string{
				"INCNAME": {"PATH(x)"},
				"PATH(x)": {"<dir/ ## x>"},
			},
			wantErr: true,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got, err := cppExpandMacros(ctx, nil, tc.incname, tc.macros)
			if gotErr := err != nil; gotErr != tc.wantErr {
				t.Errorf("expandMacros(ctx, %q, macros)=%q, %v; want _, err=%t", tc.incname, got, err, tc.wantErr)
			}
			if diff := cmp.Diff(tc.want, got); diff != "" {
				t.Errorf("expandMacros(ctx, %q, macros): diff -want +got:\n%s", tc.incname, diff)
			}
		})
	}
}
