/**
 * WXR Reference Comparison End-to-End Test
 *
 * This test parses the reference WXR file (tests/assets/wxr-ref/reduced_wxr.xml),
 * processes it through the full import pipeline (WxrParser → ImportAnalysisEngine → ImportExecutionEngine),
 * and compares each generated post/page with the expected reference markdown files.
 *
 * The test reports all differences between the generated output and reference files,
 * excluding the id and projectId fields which are runtime-generated GUIDs.
 */

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import * as path from 'path';
import * as fs from 'fs';
import matter from 'gray-matter';
import { WxrParser, type WxrData } from '../../src/main/engine/WxrParser';
import type { ImportAnalysisReport } from '../../src/main/engine/ImportAnalysisEngine';

// Read the WXR file SYNCHRONOUSLY at module load time (before mocks apply)
const wxrRefDir = path.join(__dirname, '../assets/wxr-ref');
const wxrFilePath = path.join(wxrRefDir, 'reduced_wxr.xml');
const wxrFileContent = fs.readFileSync(wxrFilePath, 'utf-8');

// Read all reference markdown files
const referenceFiles = new Map<string, string>();
const files = fs.readdirSync(wxrRefDir);
for (const file of files) {
  if (file.endsWith('.md')) {
    const content = fs.readFileSync(path.join(wxrRefDir, file), 'utf-8');
    referenceFiles.set(file.replace('.md', ''), content);
  }
}

// Track files written during import
const writtenFiles: Array<{
  path: string;
  content: string;
}> = [];

// Track all database inserts
const insertedPosts: Array<{
  id: string;
  projectId: string;
  title: string;
  slug: string;
  content: string | null;
  status: string;
  tags: string;
  categories: string;
  createdAt: Date;
  updatedAt: Date;
  publishedAt?: Date;
  author?: string;
}> = [];

const createdTags: string[] = [];

// Mock database
const mockDb = {
  insert: vi.fn().mockImplementation((table: any) => ({
    values: vi.fn().mockImplementation(async (data: any) => {
      if (data && typeof data === 'object') {
        if ('slug' in data && 'title' in data) {
          insertedPosts.push(data);
        }
      }
      return data;
    }),
  })),
  select: vi.fn().mockReturnValue({
    from: vi.fn().mockReturnValue({
      where: vi.fn().mockReturnValue({
        all: vi.fn().mockResolvedValue([]),
      }),
    }),
  }),
};

const mockClient = {
  execute: vi.fn().mockResolvedValue({ rows: [] }),
};

// Mock modules
vi.mock('../../src/main/database', () => ({
  getDatabase: vi.fn(() => ({
    getLocal: vi.fn(() => mockDb),
    getLocalClient: vi.fn(() => mockClient),
  })),
}));

vi.mock('fs/promises', () => ({
  mkdir: vi.fn().mockResolvedValue(undefined),
  writeFile: vi.fn().mockImplementation(async (filePath: string, content: string) => {
    writtenFiles.push({ path: filePath, content });
  }),
  copyFile: vi.fn().mockResolvedValue(undefined),
  access: vi.fn().mockResolvedValue(undefined),
  stat: vi.fn().mockResolvedValue({ size: 1024 }),
  readFile: vi.fn().mockImplementation(async (filePath: string) => {
    if (filePath.endsWith('reduced_wxr.xml')) {
      return wxrFileContent;
    }
    return Buffer.from('test data');
  }),
}));

vi.mock('electron', () => ({
  app: {
    getPath: vi.fn(() => '/mock/user/data'),
  },
}));

let uuidCounter = 0;
vi.mock('uuid', () => ({
  v4: vi.fn(() => `test-uuid-${++uuidCounter}`),
}));

// Mock TagEngine
const mockTagEngine = {
  setProjectContext: vi.fn(),
  createTag: vi.fn().mockImplementation(async (input: { name: string }) => {
    createdTags.push(input.name.toLowerCase());
    return {
      id: `tag-${input.name}`,
      projectId: 'test-project',
      name: input.name.toLowerCase(),
      createdAt: new Date(),
      updatedAt: new Date(),
    };
  }),
  getAllTags: vi.fn().mockResolvedValue([]),
};

vi.mock('../../src/main/engine/TagEngine', () => ({
  getTagEngine: vi.fn(() => mockTagEngine),
}));

// Mock PostEngine
const mockPostEngine = {
  setProjectContext: vi.fn(),
  createPost: vi.fn(),
  publishPost: vi.fn(),
  isSlugAvailable: vi.fn().mockResolvedValue(true),
  generateUniqueSlug: vi.fn().mockImplementation(async (title: string) => {
    return `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}-new`;
  }),
  updateFTSIndex: vi.fn().mockResolvedValue(undefined),
};

vi.mock('../../src/main/engine/PostEngine', () => ({
  getPostEngine: vi.fn(() => mockPostEngine),
}));

// Mock MediaEngine
const mockMediaEngine = {
  setProjectContext: vi.fn(),
  importMedia: vi.fn().mockImplementation(async (sourcePath: string, metadata?: any) => {
    return {
      id: `media-${Math.random().toString(36).substr(2, 9)}`,
      filename: path.basename(sourcePath),
      originalName: metadata?.originalName || path.basename(sourcePath),
      title: metadata?.title,
      linkedPostIds: metadata?.linkedPostIds || [],
    };
  }),
};

vi.mock('../../src/main/engine/MediaEngine', () => ({
  getMediaEngine: vi.fn(() => mockMediaEngine),
}));

// Mock PostMediaEngine
const mockPostMediaEngine = {
  setProjectContext: vi.fn(),
  linkMediaToPost: vi.fn().mockResolvedValue({
    id: 'link-id',
    projectId: 'test-project',
    postId: 'post-id',
    mediaId: 'media-id',
    sortOrder: 0,
    createdAt: new Date(),
  }),
};

vi.mock('../../src/main/engine/PostMediaEngine', () => ({
  getPostMediaEngine: vi.fn(() => mockPostMediaEngine),
}));

// Import after mocks are set up
import { ImportExecutionEngine } from '../../src/main/engine/ImportExecutionEngine';
import { ImportAnalysisEngine } from '../../src/main/engine/ImportAnalysisEngine';

/**
 * Parse frontmatter from a markdown file content
 */
function parseFrontmatter(content: string): { data: Record<string, unknown>; body: string } {
  const parsed = matter(content);
  return {
    data: parsed.data as Record<string, unknown>,
    body: parsed.content.trim(),
  };
}

/**
 * Compares two values and returns a description of differences
 */
function describeDifference(key: string, expected: unknown, actual: unknown): string | null {
  // Normalize arrays for comparison (sort them)
  const normalizeArray = (arr: unknown) => {
    if (Array.isArray(arr)) {
      return [...arr].sort();
    }
    return arr;
  };

  const expectedNorm = normalizeArray(expected);
  const actualNorm = normalizeArray(actual);

  // Compare stringified versions for arrays and objects
  const expectedStr = JSON.stringify(expectedNorm);
  const actualStr = JSON.stringify(actualNorm);

  if (expectedStr !== actualStr) {
    return `${key}: expected ${expectedStr}, got ${actualStr}`;
  }
  return null;
}

/**
 * Compare metadata (excluding id and projectId)
 */
function compareMetadata(
  expected: Record<string, unknown>,
  actual: Record<string, unknown>,
  slug: string
): string[] {
  const differences: string[] = [];

  // Keys to skip (GUIDs that are runtime-generated)
  const skipKeys = ['id', 'projectId'];

  // Check all expected keys
  for (const key of Object.keys(expected)) {
    if (skipKeys.includes(key)) continue;

    const diff = describeDifference(key, expected[key], actual[key]);
    if (diff) {
      differences.push(`[${slug}] Metadata ${diff}`);
    }
  }

  // Check for unexpected keys in actual (that aren't in expected and aren't skipped)
  for (const key of Object.keys(actual)) {
    if (skipKeys.includes(key)) continue;
    if (!(key in expected)) {
      differences.push(`[${slug}] Metadata: unexpected key "${key}" with value ${JSON.stringify(actual[key])}`);
    }
  }

  return differences;
}

/**
 * Compare body content
 */
function compareBody(expected: string, actual: string, slug: string): string[] {
  const differences: string[] = [];

  // Normalize whitespace for comparison
  const normalizeContent = (content: string) => {
    return content
      .replace(/\r\n/g, '\n')  // Normalize line endings
      .trim();
  };

  const expectedNorm = normalizeContent(expected);
  const actualNorm = normalizeContent(actual);

  if (expectedNorm !== actualNorm) {
    // Find specific differences
    const expectedLines = expectedNorm.split('\n');
    const actualLines = actualNorm.split('\n');

    // Report line count difference
    if (expectedLines.length !== actualLines.length) {
      differences.push(`[${slug}] Body: line count differs - expected ${expectedLines.length}, got ${actualLines.length}`);
    }

    // Find first differing line
    const minLines = Math.min(expectedLines.length, actualLines.length);
    for (let i = 0; i < minLines; i++) {
      if (expectedLines[i] !== actualLines[i]) {
        differences.push(`[${slug}] Body: line ${i + 1} differs`);
        differences.push(`  expected: "${expectedLines[i].substring(0, 100)}${expectedLines[i].length > 100 ? '...' : ''}"`);
        differences.push(`  actual:   "${actualLines[i].substring(0, 100)}${actualLines[i].length > 100 ? '...' : ''}"`);
        
        // Show byte-level differences for invisible character issues
        const expLine = expectedLines[i];
        const actLine = actualLines[i];
        const minLen = Math.min(expLine.length, actLine.length);
        let firstDiffPos = -1;
        for (let j = 0; j < minLen; j++) {
          if (expLine.charCodeAt(j) !== actLine.charCodeAt(j)) {
            firstDiffPos = j;
            break;
          }
        }
        if (firstDiffPos === -1 && expLine.length !== actLine.length) {
          firstDiffPos = minLen;
        }
        if (firstDiffPos >= 0) {
          const expCode = firstDiffPos < expLine.length ? expLine.charCodeAt(firstDiffPos) : -1;
          const actCode = firstDiffPos < actLine.length ? actLine.charCodeAt(firstDiffPos) : -1;
          const expChar = firstDiffPos < expLine.length ? expLine[firstDiffPos] : '(end)';
          const actChar = firstDiffPos < actLine.length ? actLine[firstDiffPos] : '(end)';
          differences.push(`  BYTE DIFF at pos ${firstDiffPos}: expected '${expChar}' (0x${expCode.toString(16)}) vs actual '${actChar}' (0x${actCode.toString(16)})`);
          differences.push(`  context: ...${expLine.substring(Math.max(0, firstDiffPos - 10), firstDiffPos)}[HERE]${expLine.substring(firstDiffPos, firstDiffPos + 10)}...`);
        }
        break; // Only report first difference for conciseness
      }
    }

    // If we didn't find a difference yet but line counts differ, show extra/missing lines
    if (differences.length === 1 && expectedLines.length !== actualLines.length) {
      if (actualLines.length > expectedLines.length) {
        differences.push(`[${slug}] Body: extra lines starting at line ${expectedLines.length + 1}`);
        differences.push(`  first extra line: "${actualLines[expectedLines.length]?.substring(0, 100) || ''}"`);
      } else {
        differences.push(`[${slug}] Body: missing lines starting at line ${actualLines.length + 1}`);
        differences.push(`  first missing line: "${expectedLines[actualLines.length]?.substring(0, 100) || ''}"`);
      }
    }
  }

  return differences;
}

describe('WXR Reference Comparison E2E Tests', () => {
  let executionEngine: ImportExecutionEngine;
  let analysisEngine: ImportAnalysisEngine;
  let wxrData: WxrData;

  beforeEach(async () => {
    // Reset tracking arrays
    writtenFiles.length = 0;
    insertedPosts.length = 0;
    createdTags.length = 0;
    uuidCounter = 0;

    // Clear all mocks
    vi.clearAllMocks();

    // Create engine instances
    executionEngine = new ImportExecutionEngine({
      tagEngine: mockTagEngine as any,
      postEngine: mockPostEngine as any,
      mediaEngine: mockMediaEngine as any,
      postMediaEngine: mockPostMediaEngine as any,
    });
    executionEngine.setProjectContext('test-project', '/mock/test/data');

    analysisEngine = new ImportAnalysisEngine();
    analysisEngine.setProjectContext('test-project');

    // Parse the WXR file
    const parser = new WxrParser();
    wxrData = await parser.parseFile(wxrFilePath);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('should parse the WXR file correctly', () => {
    // Verify we have posts and pages
    expect(wxrData.posts.length).toBeGreaterThan(0);
    expect(wxrData.pages.length).toBeGreaterThan(0);

    console.log(`\nParsed WXR file:`);
    console.log(`  - Posts: ${wxrData.posts.length}`);
    console.log(`  - Pages: ${wxrData.pages.length}`);
    console.log(`  - Categories: ${wxrData.categories.length}`);
    console.log(`  - Tags: ${wxrData.tags.length}`);
    console.log(`  - Reference files: ${referenceFiles.size}`);
  });

  it('should compare all posts and pages with reference files', async () => {
    // Analyze the WXR data
    const report = await analysisEngine.analyzeWxr(wxrData, wxrFilePath);

    // Execute the import
    const result = await executionEngine.executeImport(report, {});

    console.log(`\nImport completed:`);
    console.log(`  - Posts imported: ${result.posts.imported}`);
    console.log(`  - Pages imported: ${result.pages.imported}`);
    console.log(`  - Tags created: ${result.tags.created}`);
    console.log(`  - Files written: ${writtenFiles.length}`);

    // Collect all differences
    const allDifferences: string[] = [];
    const matchedSlugs: string[] = [];
    const unmatchedReferences: string[] = [];
    const unmatchedGenerated: string[] = [];

    // Build a map of generated files by slug
    const generatedBySlug = new Map<string, { path: string; content: string }>();
    for (const file of writtenFiles) {
      const filename = path.basename(file.path, '.md');
      generatedBySlug.set(filename, file);
    }

    // Compare each reference file with generated output
    for (const [slug, refContent] of referenceFiles) {
      const generated = generatedBySlug.get(slug);

      if (!generated) {
        unmatchedReferences.push(slug);
        continue;
      }

      matchedSlugs.push(slug);

      // Parse both files
      const refParsed = parseFrontmatter(refContent);
      const genParsed = parseFrontmatter(generated.content);

      // Compare metadata
      const metaDiffs = compareMetadata(refParsed.data, genParsed.data, slug);
      allDifferences.push(...metaDiffs);

      // Compare body
      const bodyDiffs = compareBody(refParsed.body, genParsed.body, slug);
      allDifferences.push(...bodyDiffs);
    }

    // Find generated files without reference
    for (const slug of generatedBySlug.keys()) {
      if (!referenceFiles.has(slug)) {
        unmatchedGenerated.push(slug);
      }
    }

    // Report results
    console.log(`\n${'='.repeat(80)}`);
    console.log('REFERENCE COMPARISON RESULTS');
    console.log('='.repeat(80));

    console.log(`\nMatched slugs: ${matchedSlugs.length}`);
    if (matchedSlugs.length > 0 && matchedSlugs.length <= 50) {
      console.log(`  ${matchedSlugs.join(', ')}`);
    }

    if (unmatchedReferences.length > 0) {
      console.log(`\nReference files without generated output (${unmatchedReferences.length}):`);
      for (const slug of unmatchedReferences) {
        console.log(`  - ${slug}`);
      }
    }

    if (unmatchedGenerated.length > 0) {
      console.log(`\nGenerated files without reference (${unmatchedGenerated.length}):`);
      for (const slug of unmatchedGenerated) {
        console.log(`  - ${slug}`);
      }
    }

    if (allDifferences.length > 0) {
      console.log(`\n${'='.repeat(80)}`);
      console.log(`DIFFERENCES FOUND: ${allDifferences.length}`);
      console.log('='.repeat(80));
      for (const diff of allDifferences) {
        console.log(diff);
      }
    } else {
      console.log(`\n${'='.repeat(80)}`);
      console.log('NO DIFFERENCES FOUND - ALL OUTPUTS MATCH REFERENCE FILES');
      console.log('='.repeat(80));
    }

    // Summary statistics
    console.log(`\n${'='.repeat(80)}`);
    console.log('SUMMARY');
    console.log('='.repeat(80));
    console.log(`Total reference files: ${referenceFiles.size}`);
    console.log(`Total generated files: ${generatedBySlug.size}`);
    console.log(`Matched: ${matchedSlugs.length}`);
    console.log(`Unmatched references: ${unmatchedReferences.length}`);
    console.log(`Unmatched generated: ${unmatchedGenerated.length}`);
    console.log(`Total differences: ${allDifferences.length}`);

    // The test MUST fail if there are any differences
    // All issues must be addressed before this test can pass
    expect(allDifferences.length).toBe(0);
  });

  it('should report detailed differences for each post/page', async () => {
    // Analyze the WXR data
    const report = await analysisEngine.analyzeWxr(wxrData, wxrFilePath);

    // Execute the import
    await executionEngine.executeImport(report, {});

    // Build a map of generated files by slug
    const generatedBySlug = new Map<string, { path: string; content: string }>();
    for (const file of writtenFiles) {
      const filename = path.basename(file.path, '.md');
      generatedBySlug.set(filename, file);
    }

    // Detailed comparison for each file
    const detailedReport: Array<{
      slug: string;
      status: 'match' | 'mismatch' | 'missing-reference' | 'missing-generated';
      metadataDiffs: string[];
      bodyDiffs: string[];
    }> = [];

    for (const [slug, refContent] of referenceFiles) {
      const generated = generatedBySlug.get(slug);

      if (!generated) {
        detailedReport.push({
          slug,
          status: 'missing-generated',
          metadataDiffs: ['File not generated during import'],
          bodyDiffs: [],
        });
        continue;
      }

      const refParsed = parseFrontmatter(refContent);
      const genParsed = parseFrontmatter(generated.content);

      const metaDiffs = compareMetadata(refParsed.data, genParsed.data, slug);
      const bodyDiffs = compareBody(refParsed.body, genParsed.body, slug);

      detailedReport.push({
        slug,
        status: metaDiffs.length === 0 && bodyDiffs.length === 0 ? 'match' : 'mismatch',
        metadataDiffs: metaDiffs,
        bodyDiffs: bodyDiffs,
      });
    }

    // Print detailed report
    console.log(`\n${'='.repeat(80)}`);
    console.log('DETAILED COMPARISON REPORT');
    console.log('='.repeat(80));

    const matches = detailedReport.filter(r => r.status === 'match');
    const mismatches = detailedReport.filter(r => r.status === 'mismatch');
    const missingGenerated = detailedReport.filter(r => r.status === 'missing-generated');

    console.log(`\nMatching files (${matches.length}):`);
    if (matches.length > 0) {
      for (const m of matches) {
        console.log(`  ✓ ${m.slug}`);
      }
    } else {
      console.log('  (none)');
    }

    console.log(`\nFiles with differences (${mismatches.length}):`);
    if (mismatches.length > 0) {
      for (const m of mismatches) {
        console.log(`\n  ✗ ${m.slug}:`);
        if (m.metadataDiffs.length > 0) {
          console.log('    Metadata differences:');
          for (const d of m.metadataDiffs) {
            console.log(`      ${d}`);
          }
        }
        if (m.bodyDiffs.length > 0) {
          console.log('    Body differences:');
          for (const d of m.bodyDiffs) {
            console.log(`      ${d}`);
          }
        }
      }
    } else {
      console.log('  (none)');
    }

    console.log(`\nMissing generated files (${missingGenerated.length}):`);
    if (missingGenerated.length > 0) {
      for (const m of missingGenerated) {
        console.log(`  ✗ ${m.slug}`);
      }
    } else {
      console.log('  (none)');
    }

    // Final summary
    console.log(`\n${'='.repeat(80)}`);
    console.log('FINAL SUMMARY');
    console.log('='.repeat(80));
    console.log(`Total files compared: ${detailedReport.length}`);
    console.log(`Perfect matches: ${matches.length}`);
    console.log(`Files with differences: ${mismatches.length}`);
    console.log(`Missing generated files: ${missingGenerated.length}`);

    // Expect the test to pass - we're just reporting
    expect(detailedReport.length).toBeGreaterThan(0);
  });
});
