using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Xunit;

namespace StellaOps.Policy.Tests;

public sealed class PolicyPreviewServiceTests
{
    [Fact]
    public async Task PreviewAsync_ComputesDiffs_ForBlockingRule()
    {
        const string yaml = """
version: "1.0"
rules:
  - name: Block Critical
    severity: [Critical]
    action: block
""";

        var snapshotRepo = new InMemoryPolicySnapshotRepository();
        var auditRepo = new InMemoryPolicyAuditRepository();
        var timeProvider = new FakeTimeProvider();
        var store = new PolicySnapshotStore(snapshotRepo, auditRepo, timeProvider, NullLogger<PolicySnapshotStore>.Instance);

        await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, null), CancellationToken.None);

        var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);

        var findings = ImmutableArray.Create(
            PolicyFinding.Create("finding-1", PolicySeverity.Critical, environment: "prod", source: "NVD"),
            PolicyFinding.Create("finding-2", PolicySeverity.Low));

        var baseline = ImmutableArray.Create(
            new PolicyVerdict("finding-1", PolicyVerdictStatus.Pass),
            new PolicyVerdict("finding-2", PolicyVerdictStatus.Pass));

        var response = await service.PreviewAsync(new PolicyPreviewRequest(
            "sha256:abc",
            findings,
            baseline),
            CancellationToken.None);

        Assert.True(response.Success);
        Assert.Equal(1, response.ChangedCount);
        var diff1 = Assert.Single(response.Diffs.Where(diff => diff.Projected.FindingId == "finding-1"));
        Assert.Equal(PolicyVerdictStatus.Pass, diff1.Baseline.Status);
        Assert.Equal(PolicyVerdictStatus.Blocked, diff1.Projected.Status);
        Assert.Equal("Block Critical", diff1.Projected.RuleName);
        Assert.True(diff1.Projected.Score > 0);
        Assert.Equal(PolicyScoringConfig.Default.Version, diff1.Projected.ConfigVersion);
        Assert.Equal(PolicyVerdictStatus.Pass, response.Diffs.First(diff => diff.Projected.FindingId == "finding-2").Projected.Status);
    }

    [Fact]
    public async Task PreviewAsync_UsesProposedPolicy_WhenProvided()
    {
        const string yaml = """
version: "1.0"
rules:
  - name: Ignore Dev
    environments: [dev]
    action:
      type: ignore
      justification: dev waiver
""";

        var snapshotRepo = new InMemoryPolicySnapshotRepository();
        var auditRepo = new InMemoryPolicyAuditRepository();
        var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
        var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);

        var findings = ImmutableArray.Create(
            PolicyFinding.Create("finding-1", PolicySeverity.Medium, environment: "dev"));

        var baseline = ImmutableArray.Create(new PolicyVerdict("finding-1", PolicyVerdictStatus.Blocked));

        var response = await service.PreviewAsync(new PolicyPreviewRequest(
            "sha256:def",
            findings,
            baseline,
            SnapshotOverride: null,
            ProposedPolicy: new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "dev override")),
            CancellationToken.None);

        Assert.True(response.Success);
        var diff = Assert.Single(response.Diffs);
        Assert.Equal(PolicyVerdictStatus.Blocked, diff.Baseline.Status);
        Assert.Equal(PolicyVerdictStatus.Ignored, diff.Projected.Status);
        Assert.Equal("Ignore Dev", diff.Projected.RuleName);
        Assert.True(diff.Projected.Score >= 0);
        Assert.Equal(1, response.ChangedCount);
    }

    [Fact]
    public async Task PreviewAsync_ReturnsIssues_WhenPolicyInvalid()
    {
        var snapshotRepo = new InMemoryPolicySnapshotRepository();
        var auditRepo = new InMemoryPolicyAuditRepository();
        var store = new PolicySnapshotStore(snapshotRepo, auditRepo, TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
        var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);

        const string invalid = "version: 1.0";
        var request = new PolicyPreviewRequest(
            "sha256:ghi",
            ImmutableArray<PolicyFinding>.Empty,
            ImmutableArray<PolicyVerdict>.Empty,
            SnapshotOverride: null,
            ProposedPolicy: new PolicySnapshotContent(invalid, PolicyDocumentFormat.Yaml, null, null, null));

        var response = await service.PreviewAsync(request, CancellationToken.None);

        Assert.False(response.Success);
        Assert.NotEmpty(response.Issues);
    }

    [Fact]
    public async Task PreviewAsync_QuietWithoutVexDowngradesToWarn()
    {
        const string yaml = """
version: "1.0"
rules:
  - name: Quiet Without VEX
    severity: [Low]
    quiet: true
    action:
      type: ignore
""";

        var binding = PolicyBinder.Bind(yaml, PolicyDocumentFormat.Yaml);
        Assert.True(binding.Success);
        Assert.Empty(binding.Issues);
        Assert.False(binding.Document.Rules[0].Metadata.ContainsKey("quiet"));
        Assert.True(binding.Document.Rules[0].Action.Quiet);

        var store = new PolicySnapshotStore(new InMemoryPolicySnapshotRepository(), new InMemoryPolicyAuditRepository(), TimeProvider.System, NullLogger<PolicySnapshotStore>.Instance);
        await store.SaveAsync(new PolicySnapshotContent(yaml, PolicyDocumentFormat.Yaml, "tester", null, "quiet test"), CancellationToken.None);
        var snapshot = await store.GetLatestAsync();
        Assert.NotNull(snapshot);
        Assert.True(snapshot!.Document.Rules[0].Action.Quiet);
        Assert.Null(snapshot.Document.Rules[0].Action.RequireVex);
        Assert.Equal(PolicyActionType.Ignore, snapshot.Document.Rules[0].Action.Type);
        var manualVerdict = PolicyEvaluation.EvaluateFinding(snapshot.Document, snapshot.ScoringConfig, PolicyFinding.Create("finding-quiet", PolicySeverity.Low));
        Assert.Equal(PolicyVerdictStatus.Warned, manualVerdict.Status);

        var service = new PolicyPreviewService(store, NullLogger<PolicyPreviewService>.Instance);

        var findings = ImmutableArray.Create(PolicyFinding.Create("finding-quiet", PolicySeverity.Low));
        var baseline = ImmutableArray<PolicyVerdict>.Empty;

        var response = await service.PreviewAsync(new PolicyPreviewRequest(
            "sha256:quiet",
            findings,
            baseline),
            CancellationToken.None);

        Assert.True(response.Success);
        var verdict = Assert.Single(response.Diffs).Projected;
        Assert.Equal(PolicyVerdictStatus.Warned, verdict.Status);
        Assert.Contains("requireVex", verdict.Notes, System.StringComparison.OrdinalIgnoreCase);
        Assert.True(verdict.Score >= 0);
    }
}
