package sandbox

import (
	"bytes"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/safedep/dry/usefulerror"
	"github.com/safedep/pmg/errcodes"
	pmgsandbox "github.com/safedep/pmg/sandbox"
)

func sampleReport() *pmgsandbox.ViolationReport {
	return &pmgsandbox.ViolationReport{
		SandboxName:   pmgsandbox.DriverSeatbelt,
		PolicyName:    "npm-restrictive",
		CorrelationID: "corr-abc",
		Violations: []pmgsandbox.Violation{
			{
				Kind:      pmgsandbox.ViolationKindFSWrite,
				Target:    "/Users/dev/project/.env",
				RuleLabel: "file-write* on sensitive project file",
				Process:   "npm",
				RawLog:    "deny file-write* /Users/dev/project/.env",
			},
		},
	}
}

func writeFixtureCache(t *testing.T, dir string) *pmgsandbox.ViolationCache {
	t.Helper()
	cache := pmgsandbox.NewViolationCache(dir, pmgsandbox.WithClock(func() time.Time {
		return time.Date(2026, 5, 14, 10, 30, 0, 0, time.UTC)
	}))
	_, err := cache.Write(sampleReport())
	require.NoError(t, err)
	return cache
}

func runExplainCmd(t *testing.T, factory cacheFactory, args []string, stdin string) (string, string, error) {
	t.Helper()
	cmd := newExplainCommand(factory)
	var out, errOut bytes.Buffer
	cmd.SetOut(&out)
	cmd.SetErr(&errOut)
	cmd.SetIn(strings.NewReader(stdin))
	cmd.SetArgs(args)
	err := cmd.Execute()
	return out.String(), errOut.String(), err
}

func TestExplainLastEmptyCache(t *testing.T) {
	dir := t.TempDir()
	factory := func() *pmgsandbox.ViolationCache { return pmgsandbox.NewViolationCache(dir) }

	stdout, _, err := runExplainCmd(t, factory, []string{"--last"}, "")
	require.Error(t, err)

	fe, ok := err.(*explainFailError)
	require.True(t, ok)
	assert.Equal(t, ExitCodeExplainFail, fe.ExitCode())
	usefulErr, ok := usefulerror.AsUsefulError(err)
	require.True(t, ok)
	assert.Equal(t, errcodes.NotFound, usefulErr.Code())
	assert.Empty(t, stdout)
	assert.Contains(t, err.Error(), "no violations cached")
}

func TestExplainLastRendersHuman(t *testing.T) {
	dir := t.TempDir()
	writeFixtureCache(t, dir)
	factory := func() *pmgsandbox.ViolationCache { return pmgsandbox.NewViolationCache(dir) }

	stdout, _, err := runExplainCmd(t, factory, []string{"--last"}, "")
	require.NoError(t, err)

	assert.Contains(t, stdout, "seatbelt")
	assert.Contains(t, stdout, "npm-restrictive")
	assert.Contains(t, stdout, "Reason:")
	assert.Contains(t, stdout, "Details:")
	assert.Contains(t, stdout, "Primary violation:")
	assert.Contains(t, stdout, "/Users/dev/project/.env")
}

func TestExplainLastJSON(t *testing.T) {
	dir := t.TempDir()
	writeFixtureCache(t, dir)
	factory := func() *pmgsandbox.ViolationCache { return pmgsandbox.NewViolationCache(dir) }

	stdout, _, err := runExplainCmd(t, factory, []string{"--last", "--json"}, "")
	require.NoError(t, err)

	var payload struct {
		Explanation struct {
			Hint              string `json:"hint"`
			Details           string `json:"details"`
			SuggestedOverride string `json:"suggested_override"`
			Primary           struct {
				Kind   string `json:"kind"`
				Target string `json:"target"`
			} `json:"primary"`
		} `json:"explanation"`
		Report     *pmgsandbox.ViolationReport `json:"report"`
		RecordedAt string                      `json:"recorded_at"`
	}
	require.NoError(t, json.Unmarshal([]byte(stdout), &payload))

	assert.NotEmpty(t, payload.Explanation.Hint)
	assert.Equal(t, "fs_write", payload.Explanation.Primary.Kind)
	assert.Equal(t, "/Users/dev/project/.env", payload.Explanation.Primary.Target)
	require.NotNil(t, payload.Report)
	assert.Equal(t, pmgsandbox.DriverSeatbelt, payload.Report.SandboxName)
	assert.NotEmpty(t, payload.RecordedAt)
}

func TestExplainLastRejectsCacheRecordWithNilReport(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "violation-99999999T999999.999999999Z-bad.json")
	body := []byte(`{"schema_version":1,"recorded_at":"2026-05-14T10:30:00Z","report":null}`)
	require.NoError(t, os.WriteFile(path, body, 0o644))
	factory := func() *pmgsandbox.ViolationCache { return pmgsandbox.NewViolationCache(dir) }

	stdout, _, err := runExplainCmd(t, factory, []string{"--last"}, "")
	require.Error(t, err)

	fe, ok := err.(*explainFailError)
	require.True(t, ok)
	assert.Equal(t, ExitCodeExplainFail, fe.ExitCode())
	assert.Empty(t, stdout)
	assert.Contains(t, err.Error(), "cache JSON is missing report")
}

func TestExplainStdinValid(t *testing.T) {
	rec := pmgsandbox.ViolationCacheRecord{
		SchemaVersion: pmgsandbox.ViolationCacheSchemaVersion,
		RecordedAt:    time.Date(2026, 5, 14, 10, 30, 0, 0, time.UTC),
		Report:        sampleReport(),
	}
	data, err := json.Marshal(rec)
	require.NoError(t, err)

	factory := func() *pmgsandbox.ViolationCache {
		t.Fatal("cache should not be consulted in stdin mode")
		return nil
	}

	stdout, _, err := runExplainCmd(t, factory, []string{"-"}, string(data))
	require.NoError(t, err)
	assert.Contains(t, stdout, "Primary violation:")
	assert.Contains(t, stdout, "/Users/dev/project/.env")
}

func TestExplainStdinInvalidJSON(t *testing.T) {
	factory := func() *pmgsandbox.ViolationCache { return nil }
	_, _, err := runExplainCmd(t, factory, []string{"-"}, "not json {{")
	require.Error(t, err)
	fe, ok := err.(*explainFailError)
	require.True(t, ok)
	assert.Equal(t, ExitCodeExplainFail, fe.ExitCode())
	assert.Contains(t, err.Error(), "parse stdin JSON")
}

func TestExplainStdinSchemaVersion(t *testing.T) {
	cases := []struct {
		name string
		body string
		want string
	}{
		{
			name: "missing schema_version",
			body: `{"report":{"SandboxName":"seatbelt","PolicyName":"p","Violations":[{"Kind":"fs_write","Target":"/x","RuleLabel":"r"}]}}`,
			want: "missing schema_version",
		},
		{
			name: "unknown schema_version",
			body: `{"schema_version":999,"report":{"SandboxName":"seatbelt","PolicyName":"p"}}`,
			want: "unknown schema_version",
		},
	}

	factory := func() *pmgsandbox.ViolationCache { return nil }

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			_, _, err := runExplainCmd(t, factory, []string{"-"}, tc.body)
			require.Error(t, err)
			fe, ok := err.(*explainFailError)
			require.True(t, ok)
			assert.Equal(t, ExitCodeExplainFail, fe.ExitCode())
			assert.Contains(t, err.Error(), tc.want)
		})
	}
}

func TestExplainMutualExclusion(t *testing.T) {
	factory := func() *pmgsandbox.ViolationCache { return nil }
	_, _, err := runExplainCmd(t, factory, []string{"--last", "-"}, "")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "mutually exclusive")
}

func TestExplainRejectsUnexpectedArgsWithUsage(t *testing.T) {
	factory := func() *pmgsandbox.ViolationCache { return nil }
	stdout, stderr, err := runExplainCmd(t, factory, []string{"one", "two"}, "")
	require.Error(t, err)
	assert.Contains(t, stderr, "accepts at most 1 arg(s), received 2")
	assert.Contains(t, stdout, "Usage:")
	assert.Contains(t, stdout, "explain [--last | -] [flags]")
	assert.Contains(t, stdout, "pmg sandbox explain --last")
}

func TestExplainNoMode(t *testing.T) {
	factory := func() *pmgsandbox.ViolationCache { return nil }
	_, _, err := runExplainCmd(t, factory, []string{}, "")
	require.Error(t, err)
	usefulErr, ok := usefulerror.AsUsefulError(err)
	require.True(t, ok)
	assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
	assert.Contains(t, err.Error(), "no input")
	assert.Contains(t, err.Error(), "--last")
}
