using System.IO;
using System.Text;
using System.Text.Json;

namespace AxCopilot.Services.Agent;

/// <summary>
/// 자동 테스트 생성 + 실행 + 결과 분석 도구.
/// 코드 변경 → 관련 테스트 자동 생성 → 실행 → 결과 기반 피드백 루프.
/// </summary>
public class TestLoopTool : IAgentTool
{
    public string Name => "test_loop";

    public string Description =>
        "코드 변경에 대한 테스트를 자동으로 생성하고 실행합니다.\n" +
        "- action=\"generate\": 변경된 파일에 대한 테스트 코드 생성 제안\n" +
        "- action=\"run\": 프로젝트의 테스트를 실행하고 결과 반환\n" +
        "- action=\"analyze\": 테스트 결과를 분석하여 수정 방향 제시\n" +
        "- action=\"auto_fix\": 테스트 실행 → 실패 파싱 → 구조화된 수정 지침 반환 (반복 수정용)\n" +
        "테스트 프레임워크를 자동 감지합니다 (xUnit, NUnit, MSTest, pytest, Jest 등).";

    public ToolParameterSchema Parameters => new()
    {
        Properties = new()
        {
            ["action"] = new ToolProperty
            {
                Type = "string",
                Description = "수행할 작업: generate | run | analyze | auto_fix",
                Enum = new() { "generate", "run", "analyze", "auto_fix" }
            },
            ["file_path"] = new ToolProperty
            {
                Type = "string",
                Description = "대상 소스 파일 경로 (generate 시 필요)"
            },
            ["test_output"] = new ToolProperty
            {
                Type = "string",
                Description = "분석할 테스트 출력 (analyze 시 필요)"
            },
        },
        Required = new() { "action" }
    };

    public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
    {
        var action = args.TryGetProperty("action", out var a) ? a.GetString() ?? "" : "";

        return action switch
        {
            "generate" => GenerateTestSuggestion(args, context),
            "run" => await RunTestsAsync(context, ct),
            "analyze" => AnalyzeTestOutput(args),
            "auto_fix" => await AutoFixAsync(context, ct),
            _ => ToolResult.Fail("action은 generate, run, analyze, auto_fix 중 하나여야 합니다.")
        };
    }

    private static ToolResult GenerateTestSuggestion(JsonElement args, AgentContext context)
    {
        var filePath = args.TryGetProperty("file_path", out var f) ? f.GetString() ?? "" : "";
        if (string.IsNullOrEmpty(filePath))
            return ToolResult.Fail("file_path가 필요합니다.");

        if (!Path.IsPathRooted(filePath) && !string.IsNullOrEmpty(context.WorkFolder))
            filePath = Path.Combine(context.WorkFolder, filePath);

        if (!File.Exists(filePath))
            return ToolResult.Fail($"파일 없음: {filePath}");

        var ext = Path.GetExtension(filePath).ToLowerInvariant();
        var (framework, testExt, convention) = ext switch
        {
            ".cs" => ("xUnit/NUnit/MSTest", ".cs", "ClassNameTests.cs"),
            ".py" => ("pytest", ".py", "test_module.py"),
            ".ts" or ".tsx" => ("Jest/Vitest", ".test.ts", "Component.test.ts"),
            ".js" or ".jsx" => ("Jest", ".test.js", "module.test.js"),
            ".java" => ("JUnit", ".java", "ClassTest.java"),
            ".go" => ("go test", "_test.go", "module_test.go"),
            _ => ("unknown", ext, "test" + ext),
        };

        var content = File.ReadAllText(filePath);
        var lineCount = content.Split('\n').Length;

        return ToolResult.Ok(
            $"테스트 생성 제안:\n" +
            $"  대상 파일: {Path.GetFileName(filePath)} ({lineCount}줄)\n" +
            $"  감지된 프레임워크: {framework}\n" +
            $"  테스트 파일 명명: {convention}\n" +
            $"  테스트 파일 확장자: {testExt}\n\n" +
            $"file_write 도구로 테스트 파일을 생성한 후, test_loop action=\"run\"으로 실행하세요.");
    }

    private static async Task<ToolResult> RunTestsAsync(AgentContext context, CancellationToken ct)
    {
        if (string.IsNullOrEmpty(context.WorkFolder))
            return ToolResult.Fail("작업 폴더가 설정되어 있지 않습니다.");

        // 프로젝트 타입 자동 감지 → 테스트 명령 결정
        var (cmd, cmdArgs) = DetectTestCommand(context.WorkFolder);
        if (cmd == null)
            return ToolResult.Fail("테스트 프레임워크를 감지할 수 없습니다. 지원: .NET (dotnet test), Python (pytest), Node.js (npm test)");

        try
        {
            var psi = new System.Diagnostics.ProcessStartInfo
            {
                FileName = cmd,
                Arguments = cmdArgs,
                WorkingDirectory = context.WorkFolder,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true,
            };

            using var proc = System.Diagnostics.Process.Start(psi);
            if (proc == null) return ToolResult.Fail("테스트 프로세스 시작 실패");

            using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            cts.CancelAfter(TimeSpan.FromSeconds(120));

            var stdout = await proc.StandardOutput.ReadToEndAsync(cts.Token);
            var stderr = await proc.StandardError.ReadToEndAsync(cts.Token);
            await proc.WaitForExitAsync(cts.Token);

            var sb = new StringBuilder();
            sb.AppendLine($"테스트 실행 결과 (exit code: {proc.ExitCode}):");
            sb.AppendLine($"명령: {cmd} {cmdArgs}");
            sb.AppendLine();
            if (!string.IsNullOrWhiteSpace(stdout)) sb.AppendLine(stdout);
            if (!string.IsNullOrWhiteSpace(stderr)) sb.AppendLine($"[STDERR]\n{stderr}");

            return proc.ExitCode == 0
                ? ToolResult.Ok(sb.ToString())
                : ToolResult.Ok($"테스트 실패 (exit code {proc.ExitCode}):\n{sb}");
        }
        catch (Exception ex)
        {
            return ToolResult.Fail($"테스트 실행 오류: {ex.Message}");
        }
    }

    private static ToolResult AnalyzeTestOutput(JsonElement args)
    {
        var output = args.TryGetProperty("test_output", out var o) ? o.GetString() ?? "" : "";
        if (string.IsNullOrEmpty(output))
            return ToolResult.Fail("test_output이 필요합니다.");

        var sb = new StringBuilder();
        sb.AppendLine("테스트 결과 분석:");

        var lines = output.Split('\n');
        var failedCount = lines.Count(l => l.Contains("FAIL", StringComparison.OrdinalIgnoreCase) || l.Contains("FAILED"));
        var passedCount = lines.Count(l => l.Contains("PASS", StringComparison.OrdinalIgnoreCase) || l.Contains("PASSED"));
        var errorLines = lines.Where(l => l.Contains("Error", StringComparison.OrdinalIgnoreCase) || l.Contains("Exception")).Take(10).ToList();

        sb.AppendLine($"  통과: {passedCount}개, 실패: {failedCount}개");

        if (errorLines.Count > 0)
        {
            sb.AppendLine("\n주요 오류:");
            foreach (var line in errorLines)
                sb.AppendLine($"  {line.Trim()}");
        }

        if (failedCount > 0)
            sb.AppendLine("\n다음 단계: 실패한 테스트를 확인하고 관련 코드를 수정한 후 test_loop action=\"run\"으로 다시 실행하세요.");
        else
            sb.AppendLine("\n모든 테스트가 통과했습니다.");

        return ToolResult.Ok(sb.ToString());
    }

    /// <summary>
    /// auto_fix: 테스트 실행 → 실패 파싱 → 구조화된 수정 지침 반환.
    /// LLM이 이 결과를 받아 코드를 수정하고 다시 auto_fix를 호출하는 반복 루프를 형성합니다.
    /// </summary>
    private static async Task<ToolResult> AutoFixAsync(AgentContext context, CancellationToken ct)
    {
        // 1. 테스트 실행
        var runResult = await RunTestsAsync(context, ct);
        var output = runResult.Output;

        // 2. 테스트 전체 통과 → 성공 종료
        var lines = output.Split('\n');
        var failedCount = lines.Count(l =>
            l.Contains("FAIL", StringComparison.OrdinalIgnoreCase) ||
            l.Contains("FAILED", StringComparison.OrdinalIgnoreCase));

        if (failedCount == 0 && runResult.Success && output.Contains("exit code: 0"))
            return ToolResult.Ok("[AUTO_FIX: ALL_PASSED]\n모든 테스트가 통과했습니다. 수정 루프를 종료하세요.");

        // 3. 실패 파싱 → 구조화된 실패 정보
        var sb = new StringBuilder();
        sb.AppendLine("[AUTO_FIX: FAILURES_DETECTED]");
        sb.AppendLine($"실패 테스트 수: {failedCount}");
        sb.AppendLine();

        // 오류 메시지 추출
        var errors = ExtractFailureDetails(lines);
        if (errors.Count > 0)
        {
            sb.AppendLine("## 실패 상세:");
            foreach (var err in errors.Take(10))
            {
                sb.AppendLine($"- 테스트: {err.TestName}");
                if (!string.IsNullOrEmpty(err.FilePath))
                    sb.AppendLine($"  파일: {err.FilePath}:{err.Line}");
                sb.AppendLine($"  오류: {err.Message}");
                sb.AppendLine();
            }
        }

        sb.AppendLine("## 수정 지침:");
        sb.AppendLine("1. 위 오류 메시지에서 원인을 파악하세요");
        sb.AppendLine("2. file_read로 관련 파일을 읽고 오류 원인을 확인하세요");
        sb.AppendLine("3. file_edit로 코드를 수정하세요");
        sb.AppendLine("4. test_loop action=\"auto_fix\"를 다시 호출하여 결과를 확인하세요");
        sb.AppendLine("5. 모든 테스트가 통과할 때까지 반복하세요");

        // 설정에서 최대 반복 횟수 안내
        var maxIter = context.Llm?.MaxTestFixIterations ?? 5;
        sb.AppendLine($"\n※ 최대 수정 반복 횟수: {maxIter}회. 초과 시 사용자에게 보고하세요.");

        // 전체 테스트 출력 (잘라서)
        sb.AppendLine("\n## 전체 테스트 출력:");
        var truncated = output.Length > 3000 ? output[..3000] + "\n... (출력 일부 생략)" : output;
        sb.AppendLine(truncated);

        return ToolResult.Ok(sb.ToString());
    }

    /// <summary>테스트 출력에서 실패 상세를 추출합니다.</summary>
    private static List<FailureDetail> ExtractFailureDetails(string[] lines)
    {
        var failures = new List<FailureDetail>();
        FailureDetail? current = null;

        for (int i = 0; i < lines.Length; i++)
        {
            var line = lines[i].Trim();

            // .NET: "X 실패 테스트명 [시간]" 또는 "Failed 테스트명"
            if (line.StartsWith("Failed ", StringComparison.OrdinalIgnoreCase) ||
                line.Contains("FAIL!", StringComparison.OrdinalIgnoreCase))
            {
                current = new FailureDetail { TestName = line };
                failures.Add(current);
            }
            // pytest: "FAILED test_file.py::test_name"
            else if (line.StartsWith("FAILED ", StringComparison.OrdinalIgnoreCase))
            {
                current = new FailureDetail { TestName = line[7..].Trim() };
                failures.Add(current);
            }
            // 파일 경로 + 줄 번호 패턴: "file.cs(123," or "file.py:123:"
            else if (current != null && string.IsNullOrEmpty(current.FilePath))
            {
                var pathMatch = System.Text.RegularExpressions.Regex.Match(line,
                    @"([^\s]+\.\w+)[:\(](\d+)");
                if (pathMatch.Success)
                {
                    current.FilePath = pathMatch.Groups[1].Value;
                    current.Line = int.Parse(pathMatch.Groups[2].Value);
                }
            }
            // 오류 메시지: "Assert.", "Error:", "Exception:"
            else if (current != null && string.IsNullOrEmpty(current.Message) &&
                     (line.Contains("Assert", StringComparison.OrdinalIgnoreCase) ||
                      line.Contains("Error", StringComparison.OrdinalIgnoreCase) ||
                      line.Contains("Exception", StringComparison.OrdinalIgnoreCase)))
            {
                current.Message = line;
            }
        }

        return failures;
    }

    private class FailureDetail
    {
        public string TestName { get; set; } = "";
        public string FilePath { get; set; } = "";
        public int Line { get; set; }
        public string Message { get; set; } = "";
    }

    private static (string? Cmd, string Args) DetectTestCommand(string workFolder)
    {
        // .NET
        if (Directory.EnumerateFiles(workFolder, "*.csproj", SearchOption.AllDirectories).Any() ||
            Directory.EnumerateFiles(workFolder, "*.sln", SearchOption.TopDirectoryOnly).Any())
            return ("dotnet", "test --no-build --verbosity normal");

        // Python
        if (File.Exists(Path.Combine(workFolder, "pytest.ini")) ||
            File.Exists(Path.Combine(workFolder, "setup.py")) ||
            Directory.EnumerateFiles(workFolder, "test_*.py", SearchOption.AllDirectories).Any())
            return ("pytest", "--tb=short -q");

        // Node.js
        if (File.Exists(Path.Combine(workFolder, "package.json")))
            return ("npm", "test -- --passWithNoTests");

        // Go
        if (Directory.EnumerateFiles(workFolder, "*_test.go", SearchOption.AllDirectories).Any())
            return ("go", "test ./...");

        return (null, "");
    }
}
