package formatters_test

import (
	"net/http"
	"os"
	"testing"

	"github.com/oasdiff/oasdiff/checker"
	"github.com/oasdiff/oasdiff/formatters"
	"github.com/oasdiff/oasdiff/load"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var gitHubFormatter = formatters.GitHubActionsFormatter{
	Localizer: MockLocalizer,
}

func TestGithubActionsLookup(t *testing.T) {
	f, err := formatters.Lookup(string(formatters.FormatGithubActions), formatters.DefaultFormatterOpts())
	require.NoError(t, err)
	require.IsType(t, formatters.GitHubActionsFormatter{}, f)
}

func TestGitHubActionsFormatter_RenderChangelog_OneFailure(t *testing.T) {
	testChanges := checker.Changes{
		checker.ApiChange{
			Id:        "change_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
	}

	// check output
	output, err := gitHubFormatter.RenderChangelog(testChanges, formatters.NewRenderOpts(), "", "")
	assert.NoError(t, err)
	expectedOutput := "::error title=change_id,file=openapi.yaml::in API GET /api/test This is a breaking change.\n"
	assert.Equal(t, expectedOutput, string(output))
}

func TestGitHubActionsFormatter_RenderChangelog_MultipleLevels(t *testing.T) {
	testChanges := checker.Changes{
		checker.ApiChange{
			Id:        "change_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
		checker.ApiChange{
			Id:        "warning_id",
			Level:     checker.WARN,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
		checker.ApiChange{
			Id:        "notice_id",
			Level:     checker.INFO,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
	}

	// check output
	output, err := gitHubFormatter.RenderChangelog(testChanges, formatters.NewRenderOpts(), "", "")
	assert.NoError(t, err)
	expectedOutput := "::error title=change_id,file=openapi.yaml::in API GET /api/test This is a breaking change.\n::warning title=warning_id,file=openapi.yaml::in API GET /api/test This is a warning.\n::notice title=notice_id,file=openapi.yaml::in API GET /api/test This is a notice.\n"
	assert.Equal(t, expectedOutput, string(output))
}

func TestGitHubActionsFormatter_DontRenderHttpSource(t *testing.T) {
	testChanges := checker.Changes{
		checker.ApiChange{
			Id:        "change_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("https://google.com/openapi.yaml"),
		},
	}

	// check output
	output, err := gitHubFormatter.RenderChangelog(testChanges, formatters.NewRenderOpts(), "", "")
	assert.NoError(t, err)
	expectedOutput := "::error title=change_id::in API GET /api/test This is a breaking change.\n"
	assert.Equal(t, expectedOutput, string(output))
}

func TestGitHubActionsFormatter_RenderChangelog_MultilineText(t *testing.T) {
	testChanges := checker.Changes{
		checker.ApiChange{
			Id:        "change_two_lines_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
	}

	// check output
	output, err := gitHubFormatter.RenderChangelog(testChanges, formatters.NewRenderOpts(), "", "")
	assert.NoError(t, err)
	expectedOutput := "::error title=change_two_lines_id,file=openapi.yaml::in API GET /api/test This is a breaking change.%0AThis is a second line.\n"
	assert.Equal(t, expectedOutput, string(output))
}

func TestGitHubActionsFormatter_RenderChangelog_FileLocation(t *testing.T) {
	testChanges := checker.Changes{
		checker.ApiChange{
			Id:        "change_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
			CommonChange: checker.CommonChange{
				RevisionSource: &checker.Source{
					File:   "openapi.json",
					Line:   21,
					Column: 6,
				},
			},
		},
	}

	// check output
	output, err := gitHubFormatter.RenderChangelog(testChanges, formatters.NewRenderOpts(), "", "")
	assert.NoError(t, err)
	expectedOutput := "::error title=change_id,file=openapi.json,line=21,col=6::in API GET /api/test This is a breaking change.\n"
	assert.Equal(t, expectedOutput, string(output))
}

func TestGitHubActionsFormatter_RenderChangelog_JobOutputParameters(t *testing.T) {
	// temp file to mock GITHUB_OUTPUT
	tempFile, err := os.CreateTemp("", "github-output")
	assert.NoError(t, err)
	defer os.Remove(tempFile.Name()) //nolint:errcheck
	_ = os.Setenv("GITHUB_OUTPUT", tempFile.Name())

	testChanges := checker.Changes{
		checker.ApiChange{
			Id:        "change_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
		checker.ApiChange{
			Id:        "change_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
		checker.ApiChange{
			Id:        "warning_id",
			Level:     checker.WARN,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
		checker.ApiChange{
			Id:        "notice_id",
			Level:     checker.INFO,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
		},
	}

	// check output
	output, err := gitHubFormatter.RenderChangelog(testChanges, formatters.NewRenderOpts(), "", "")
	assert.NoError(t, err)
	_ = os.Unsetenv("GITHUB_OUTPUT")
	expectedOutput := "::error title=change_id,file=openapi.yaml::in API GET /api/test This is a breaking change.\n::error title=change_id,file=openapi.yaml::in API GET /api/test This is a breaking change.\n::warning title=warning_id,file=openapi.yaml::in API GET /api/test This is a warning.\n::notice title=notice_id,file=openapi.yaml::in API GET /api/test This is a notice.\n"
	assert.Equal(t, expectedOutput, string(output))

	// check job output parameters (NOTE: order of parameters is not guaranteed)
	outputFile, err := os.ReadFile(tempFile.Name())
	assert.NoError(t, err)
	assert.Contains(t, string(outputFile), "error_count=2\n")
	assert.Contains(t, string(outputFile), "warning_count=1\n")
	assert.Contains(t, string(outputFile), "info_count=1\n")
}

// Property values are escaped per GitHub's workflow-command rules: ':' and
// ',' (which a git-ref source path can contain) must not leak through, or
// they would break the annotation's property syntax.
func TestGitHubActionsFormatter_RenderChangelog_EscapesProperties(t *testing.T) {
	testChanges := checker.Changes{
		checker.ApiChange{
			Id:        "change_id",
			Level:     checker.ERR,
			Operation: http.MethodGet,
			Path:      "/api/test",
			Source:    load.NewSource("openapi.yaml"),
			CommonChange: checker.CommonChange{
				RevisionSource: &checker.Source{File: "main:spec,v2.yaml", Line: 1, Column: 1},
			},
		},
	}

	output, err := gitHubFormatter.RenderChangelog(testChanges, formatters.NewRenderOpts(), "", "")
	require.NoError(t, err)
	expectedOutput := "::error title=change_id,file=main%3Aspec%2Cv2.yaml,line=1,col=1::in API GET /api/test This is a breaking change.\n"
	assert.Equal(t, expectedOutput, string(output))
}

// validate findings escape both the message ('%' and newlines) and the
// property values (a git-ref source's ':').
func TestGitHubActionsFormatter_RenderValidate_Escaping(t *testing.T) {
	findings := formatters.Findings{
		{
			Id:     "rule-id",
			Text:   "bad value 50% off\nsecond line",
			Level:  checker.ERR,
			Source: formatters.Source{File: "main:openapi.yaml", Line: 3, Column: 5},
		},
	}

	output, err := gitHubFormatter.RenderValidate(findings, formatters.NewRenderOpts())
	require.NoError(t, err)
	expectedOutput := "::error title=rule-id,file=main%3Aopenapi.yaml,line=3,col=5::bad value 50%25 off%0Asecond line\n"
	assert.Equal(t, expectedOutput, string(output))
}

func TestGitHubActionsFormatter_NotImplemented(t *testing.T) {
	var err error
	_, err = gitHubFormatter.RenderDiff(nil, formatters.NewRenderOpts())
	assert.Error(t, err)

	_, err = gitHubFormatter.RenderSummary(nil, formatters.NewRenderOpts())
	assert.Error(t, err)

	_, err = gitHubFormatter.RenderChecks(nil, formatters.NewRenderOpts())
	assert.Error(t, err)

	_, err = gitHubFormatter.RenderFlatten(nil, formatters.NewRenderOpts())
	assert.Error(t, err)
}
