#if SELFTEST
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace TIASshot {

    /// <summary>
    /// セルフテスト項目の判定（SELFTEST 限定）．
    /// </summary>
    internal enum SelfTestStatus {
        Pass,
        Fail,
    }

    /// <summary>
    /// セルフテスト 1 項目分の記録（SELFTEST 限定）．
    /// Fail を 1 つでも記録した項目は FAIL となる．Warn は判定に影響しない参考情報．
    /// </summary>
    internal sealed class SelfTestCheck {
        public string Id { get; }
        public string Title { get; }
        public List<string> Lines { get; } = new List<string>();
        public int FailCount { get; private set; }
        public int WarnCount { get; private set; }

        public SelfTestStatus Status => FailCount == 0 ? SelfTestStatus.Pass : SelfTestStatus.Fail;

        public SelfTestCheck(string id, string title) {
            Id = id;
            Title = title;
        }

        public void Info(string msg) => Lines.Add($"  - {msg}");

        public void Warn(string msg) {
            WarnCount++;
            Lines.Add($"  WARN: {msg}");
        }

        public void Fail(string msg) {
            FailCount++;
            Lines.Add($"  FAIL: {msg}");
        }

        /// <summary>条件が偽なら Fail を記録する．真なら ok メッセージを Info として残す（任意）．</summary>
        public bool Expect(bool condition, string failMsg, string okMsg = null) {
            if (!condition) {
                Fail(failMsg);
                return false;
            }
            if (okMsg != null) Info(okMsg);
            return true;
        }

        /// <summary>想定外の例外を詳細（例外型・メッセージ・アセンブリ名・Fusion ログ）付きで Fail として記録する．</summary>
        public void FailException(string context, Exception ex) {
            Fail($"{context}: {SelfTestReport.DescribeException(ex)}");
        }
    }

    /// <summary>
    /// セルフテスト結果の出力先（SELFTEST 限定）．
    /// WinForms アプリ（WinExe）は標準出力がコンソールに繋がらないため，
    /// (1) 標準出力が既にリダイレクトされていればそこへ，無ければ親プロセスのコンソールへ AttachConsole して書き，
    /// (2) 同じ内容を結果ファイル（UTF-8 BOM 付き）にも逐次書き出す（途中でハングしても書けた分は残る）．
    /// verifier 等の自動判定は結果ファイルと終了コードを正とする．
    /// </summary>
    internal sealed class SelfTestReport : IDisposable {

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool AttachConsole(int dwProcessId);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr GetStdHandle(int nStdHandle);

        private const int ATTACH_PARENT_PROCESS = -1;
        private const int STD_OUTPUT_HANDLE = -11;

        private readonly object _lock = new object();
        private readonly StreamWriter _file;
        private readonly bool _consoleAvailable;

        public string OutputPath { get; }

        public SelfTestReport(string outputPath) {
            OutputPath = outputPath;
            _consoleAvailable = PrepareConsole();

            var dir = Path.GetDirectoryName(outputPath);
            if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
            _file = new StreamWriter(outputPath, false, new UTF8Encoding(true)) { AutoFlush = true };
        }

        /// <summary>
        /// 標準出力の準備．リダイレクト済み（パイプ・ファイル）ならそのまま使い，
        /// 未接続なら親プロセスのコンソールにアタッチする．どちらも無ければファイル出力のみとする．
        /// </summary>
        private static bool PrepareConsole() {
            var handle = GetStdHandle(STD_OUTPUT_HANDLE);
            if (handle != IntPtr.Zero && handle != new IntPtr(-1)) return true;
            return AttachConsole(ATTACH_PARENT_PROCESS);
        }

        public void WriteLine(string line = "") {
            lock (_lock) {
                _file.WriteLine(line);
                if (_consoleAvailable) {
                    try {
                        Console.Out.WriteLine(line);
                        Console.Out.Flush();
                    } catch (IOException ex) {
                        // コンソール側の失敗（パイプ切断等）は結果ファイルに残す（判定は結果ファイルで行える）
                        _file.WriteLine($"  (標準出力への書き込みに失敗: {ex.Message})");
                    }
                }
            }
        }

        public void WriteCheck(SelfTestCheck check) {
            WriteLine($"[{(check.Status == SelfTestStatus.Pass ? "PASS" : "FAIL")}] {check.Id}: {check.Title}");
            foreach (var line in check.Lines) WriteLine(line);
        }

        public void Dispose() {
            lock (_lock) {
                _file.Dispose();
            }
        }

        /// <summary>
        /// 例外を 1 行に要約する．内部例外を辿り，アセンブリ解決系（FileNotFound / FileLoad / BadImageFormat）は
        /// 対象アセンブリ名と Fusion ログの要点を，型ロード系は LoaderExceptions を含める．
        /// </summary>
        public static string DescribeException(Exception ex) {
            var sb = new StringBuilder();
            var depth = 0;
            for (var e = ex; e != null && depth < 6; e = e.InnerException, depth++) {
                if (depth > 0) sb.Append(" <- inner: ");
                sb.Append($"{e.GetType().FullName}: {e.Message}");
                switch (e) {
                    case FileNotFoundException fnf:
                        AppendAssemblyDetail(sb, fnf.FileName, fnf.FusionLog);
                        break;
                    case FileLoadException fl:
                        AppendAssemblyDetail(sb, fl.FileName, fl.FusionLog);
                        break;
                    case BadImageFormatException bif:
                        AppendAssemblyDetail(sb, bif.FileName, bif.FusionLog);
                        break;
                    case System.Reflection.ReflectionTypeLoadException rtl:
                        foreach (var le in rtl.LoaderExceptions.Where(x => x != null).Take(5)) {
                            sb.Append($" [LoaderException: {le.GetType().Name}: {le.Message}]");
                        }
                        break;
                }
            }
            var frames = (ex.StackTrace ?? "")
                .Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
                .Take(4)
                .Select(x => x.Trim());
            var stack = string.Join(" | ", frames);
            if (stack.Length > 0) sb.Append($" [stack: {stack}]");
            return sb.ToString();
        }

        private static void AppendAssemblyDetail(StringBuilder sb, string fileName, string fusionLog) {
            if (!string.IsNullOrEmpty(fileName)) sb.Append($" [assembly: {fileName}]");
            if (string.IsNullOrEmpty(fusionLog)) return;
            // Fusion ログは長いため，参照・リダイレクト・失敗理由に関する行だけを抜粋する
            var keyLines = fusionLog
                .Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
                .Where(l => l.Contains("LOG: Post-policy reference") || l.Contains("LOG: Redirect")
                         || l.Contains("WRN:") || l.Contains("ERR:") || l.Contains("Calling assembly"))
                .Take(6)
                .Select(l => l.Trim());
            var joined = string.Join(" / ", keyLines);
            if (joined.Length > 0) sb.Append($" [fusion: {joined}]");
        }
    }
}
#endif
